1/* 2 * Copyright (C) 2014 Square, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.squareup.okhttp.internal.http; 18 19import com.squareup.okhttp.OkHttpClient; 20import com.squareup.okhttp.ResponseSource; 21import com.squareup.okhttp.internal.SslContextBuilder; 22import com.squareup.okhttp.mockwebserver.MockResponse; 23import com.squareup.okhttp.mockwebserver.MockWebServer; 24import com.squareup.okhttp.mockwebserver.RecordedRequest; 25import java.io.BufferedReader; 26import java.io.ByteArrayInputStream; 27import java.io.ByteArrayOutputStream; 28import java.io.FileNotFoundException; 29import java.io.IOException; 30import java.io.InputStream; 31import java.io.InputStreamReader; 32import java.io.OutputStream; 33import java.net.CacheRequest; 34import java.net.CacheResponse; 35import java.net.CookieManager; 36import java.net.HttpCookie; 37import java.net.HttpURLConnection; 38import java.net.ResponseCache; 39import java.net.SecureCacheResponse; 40import java.net.URI; 41import java.net.URISyntaxException; 42import java.net.URL; 43import java.net.URLConnection; 44import java.security.Principal; 45import java.security.cert.Certificate; 46import java.text.DateFormat; 47import java.text.SimpleDateFormat; 48import java.util.ArrayList; 49import java.util.Arrays; 50import java.util.Date; 51import java.util.HashMap; 52import java.util.Iterator; 53import java.util.List; 54import java.util.Locale; 55import java.util.Map; 56import java.util.TimeZone; 57import java.util.concurrent.TimeUnit; 58import java.util.concurrent.atomic.AtomicReference; 59import java.util.zip.GZIPOutputStream; 60import javax.net.ssl.HostnameVerifier; 61import javax.net.ssl.HttpsURLConnection; 62import javax.net.ssl.SSLContext; 63import javax.net.ssl.SSLPeerUnverifiedException; 64import javax.net.ssl.SSLSession; 65import org.junit.After; 66import org.junit.Before; 67import org.junit.Test; 68 69import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_END; 70import static org.junit.Assert.assertEquals; 71import static org.junit.Assert.assertFalse; 72import static org.junit.Assert.assertNotNull; 73import static org.junit.Assert.assertNull; 74import static org.junit.Assert.assertSame; 75import static org.junit.Assert.assertTrue; 76import static org.junit.Assert.fail; 77 78/** 79 * Tests for interaction between OkHttp and the ResponseCache. This test is based on 80 * {@link com.squareup.okhttp.internal.http.HttpResponseCacheTest}. Some tests for the 81 * {@link com.squareup.okhttp.OkResponseCache} found in HttpResponseCacheTest provide 82 * coverage for ResponseCache as well. 83 */ 84public final class ResponseCacheTest { 85 private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() { 86 @Override public boolean verify(String s, SSLSession sslSession) { 87 return true; 88 } 89 }; 90 91 private static final SSLContext sslContext = SslContextBuilder.localhost(); 92 93 private OkHttpClient client; 94 private MockWebServer server; 95 private MockWebServer server2; 96 private ResponseCache cache; 97 98 @Before public void setUp() throws Exception { 99 server = new MockWebServer(); 100 server.setNpnEnabled(false); 101 server2 = new MockWebServer(); 102 103 client = new OkHttpClient(); 104 cache = new InMemoryResponseCache(); 105 ResponseCache.setDefault(cache); 106 } 107 108 @After public void tearDown() throws Exception { 109 server.shutdown(); 110 server2.shutdown(); 111 CookieManager.setDefault(null); 112 } 113 114 private HttpURLConnection openConnection(URL url) { 115 return client.open(url); 116 } 117 118 @Test public void responseCacheAccessWithOkHttpMember() throws IOException { 119 ResponseCache.setDefault(null); 120 client.setResponseCache(cache); 121 assertSame(cache, client.getResponseCache()); 122 assertTrue(client.getOkResponseCache() instanceof ResponseCacheAdapter); 123 } 124 125 @Test public void responseCacheAccessWithGlobalDefault() throws IOException { 126 ResponseCache.setDefault(cache); 127 client.setResponseCache(null); 128 assertNull(client.getOkResponseCache()); 129 assertNull(client.getResponseCache()); 130 } 131 132 @Test public void responseCachingAndInputStreamSkipWithFixedLength() throws IOException { 133 testResponseCaching(TransferKind.FIXED_LENGTH); 134 } 135 136 @Test public void responseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException { 137 testResponseCaching(TransferKind.CHUNKED); 138 } 139 140 @Test public void responseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException { 141 testResponseCaching(TransferKind.END_OF_STREAM); 142 } 143 144 /** 145 * HttpURLConnection.getInputStream().skip(long) causes ResponseCache corruption 146 * http://code.google.com/p/android/issues/detail?id=8175 147 */ 148 private void testResponseCaching(TransferKind transferKind) throws IOException { 149 MockResponse response = 150 new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 151 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 152 .setStatus("HTTP/1.1 200 Fantastic"); 153 transferKind.setBody(response, "I love puppies but hate spiders", 1); 154 server.enqueue(response); 155 server.play(); 156 157 // Make sure that calling skip() doesn't omit bytes from the cache. 158 HttpURLConnection urlConnection = openConnection(server.getUrl("/")); 159 InputStream in = urlConnection.getInputStream(); 160 assertEquals("I love ", readAscii(urlConnection, "I love ".length())); 161 reliableSkip(in, "puppies but hate ".length()); 162 assertEquals("spiders", readAscii(urlConnection, "spiders".length())); 163 assertEquals(-1, in.read()); 164 in.close(); 165 166 urlConnection = openConnection(server.getUrl("/")); // cached! 167 in = urlConnection.getInputStream(); 168 assertEquals("I love puppies but hate spiders", 169 readAscii(urlConnection, "I love puppies but hate spiders".length())); 170 assertEquals(200, urlConnection.getResponseCode()); 171 assertEquals("Fantastic", urlConnection.getResponseMessage()); 172 173 assertEquals(-1, in.read()); 174 in.close(); 175 } 176 177 @Test public void secureResponseCaching() throws IOException { 178 server.useHttps(sslContext.getSocketFactory(), false); 179 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 180 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 181 .setBody("ABC")); 182 server.play(); 183 184 HttpsURLConnection c1 = (HttpsURLConnection) openConnection(server.getUrl("/")); 185 c1.setSSLSocketFactory(sslContext.getSocketFactory()); 186 c1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); 187 assertEquals("ABC", readAscii(c1)); 188 189 // OpenJDK 6 fails on this line, complaining that the connection isn't open yet 190 String suite = c1.getCipherSuite(); 191 List<Certificate> localCerts = toListOrNull(c1.getLocalCertificates()); 192 List<Certificate> serverCerts = toListOrNull(c1.getServerCertificates()); 193 Principal peerPrincipal = c1.getPeerPrincipal(); 194 Principal localPrincipal = c1.getLocalPrincipal(); 195 196 HttpsURLConnection c2 = (HttpsURLConnection) openConnection(server.getUrl("/")); // cached! 197 c2.setSSLSocketFactory(sslContext.getSocketFactory()); 198 c2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); 199 assertEquals("ABC", readAscii(c2)); 200 201 assertEquals(suite, c2.getCipherSuite()); 202 assertEquals(localCerts, toListOrNull(c2.getLocalCertificates())); 203 assertEquals(serverCerts, toListOrNull(c2.getServerCertificates())); 204 assertEquals(peerPrincipal, c2.getPeerPrincipal()); 205 assertEquals(localPrincipal, c2.getLocalPrincipal()); 206 } 207 208 @Test public void cacheReturnsInsecureResponseForSecureRequest() throws IOException { 209 server.useHttps(sslContext.getSocketFactory(), false); 210 server.enqueue(new MockResponse().setBody("ABC")); 211 server.enqueue(new MockResponse().setBody("DEF")); 212 server.play(); 213 214 client.setResponseCache(new InsecureResponseCache(new InMemoryResponseCache())); 215 216 HttpsURLConnection connection1 = (HttpsURLConnection) openConnection(server.getUrl("/")); 217 connection1.setSSLSocketFactory(sslContext.getSocketFactory()); 218 connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); 219 assertEquals("ABC", readAscii(connection1)); 220 221 // Not cached! 222 HttpsURLConnection connection2 = (HttpsURLConnection) openConnection(server.getUrl("/")); 223 connection2.setSSLSocketFactory(sslContext.getSocketFactory()); 224 connection2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); 225 assertEquals("DEF", readAscii(connection2)); 226 } 227 228 @Test public void responseCachingAndRedirects() throws Exception { 229 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 230 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 231 .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) 232 .addHeader("Location: /foo")); 233 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 234 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 235 .setBody("ABC")); 236 server.enqueue(new MockResponse().setBody("DEF")); 237 server.play(); 238 239 HttpURLConnection connection = openConnection(server.getUrl("/")); 240 assertEquals("ABC", readAscii(connection)); 241 242 connection = openConnection(server.getUrl("/")); // cached! 243 assertEquals("ABC", readAscii(connection)); 244 } 245 246 @Test public void redirectToCachedResult() throws Exception { 247 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60").setBody("ABC")); 248 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) 249 .addHeader("Location: /foo")); 250 server.enqueue(new MockResponse().setBody("DEF")); 251 server.play(); 252 253 assertEquals("ABC", readAscii(openConnection(server.getUrl("/foo")))); 254 RecordedRequest request1 = server.takeRequest(); 255 assertEquals("GET /foo HTTP/1.1", request1.getRequestLine()); 256 assertEquals(0, request1.getSequenceNumber()); 257 258 assertEquals("ABC", readAscii(openConnection(server.getUrl("/bar")))); 259 RecordedRequest request2 = server.takeRequest(); 260 assertEquals("GET /bar HTTP/1.1", request2.getRequestLine()); 261 assertEquals(1, request2.getSequenceNumber()); 262 263 // an unrelated request should reuse the pooled connection 264 assertEquals("DEF", readAscii(openConnection(server.getUrl("/baz")))); 265 RecordedRequest request3 = server.takeRequest(); 266 assertEquals("GET /baz HTTP/1.1", request3.getRequestLine()); 267 assertEquals(2, request3.getSequenceNumber()); 268 } 269 270 @Test public void secureResponseCachingAndRedirects() throws IOException { 271 server.useHttps(sslContext.getSocketFactory(), false); 272 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 273 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 274 .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) 275 .addHeader("Location: /foo")); 276 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 277 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 278 .setBody("ABC")); 279 server.enqueue(new MockResponse().setBody("DEF")); 280 server.play(); 281 282 client.setSslSocketFactory(sslContext.getSocketFactory()); 283 client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); 284 285 HttpsURLConnection connection1 = (HttpsURLConnection) openConnection(server.getUrl("/")); 286 assertEquals("ABC", readAscii(connection1)); 287 assertNotNull(connection1.getCipherSuite()); 288 289 // Cached! 290 HttpsURLConnection connection2 = (HttpsURLConnection) openConnection(server.getUrl("/")); 291 assertEquals("ABC", readAscii(connection2)); 292 assertNotNull(connection2.getCipherSuite()); 293 294 assertEquals(connection1.getCipherSuite(), connection2.getCipherSuite()); 295 } 296 297 /** 298 * We've had bugs where caching and cross-protocol redirects yield class 299 * cast exceptions internal to the cache because we incorrectly assumed that 300 * HttpsURLConnection was always HTTPS and HttpURLConnection was always HTTP; 301 * in practice redirects mean that each can do either. 302 * 303 * https://github.com/square/okhttp/issues/214 304 */ 305 @Test public void secureResponseCachingAndProtocolRedirects() throws IOException { 306 server2.useHttps(sslContext.getSocketFactory(), false); 307 server2.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 308 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 309 .setBody("ABC")); 310 server2.enqueue(new MockResponse().setBody("DEF")); 311 server2.play(); 312 313 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 314 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 315 .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) 316 .addHeader("Location: " + server2.getUrl("/"))); 317 server.play(); 318 319 client.setSslSocketFactory(sslContext.getSocketFactory()); 320 client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); 321 322 HttpURLConnection connection1 = openConnection(server.getUrl("/")); 323 assertEquals("ABC", readAscii(connection1)); 324 325 // Cached! 326 HttpURLConnection connection2 = openConnection(server.getUrl("/")); 327 assertEquals("ABC", readAscii(connection2)); 328 } 329 330 @Test public void responseCacheRequestHeaders() throws IOException, URISyntaxException { 331 server.enqueue(new MockResponse().setBody("ABC")); 332 server.play(); 333 334 final AtomicReference<Map<String, List<String>>> requestHeadersRef = 335 new AtomicReference<Map<String, List<String>>>(); 336 client.setResponseCache(new ResponseCache() { 337 @Override 338 public CacheResponse get(URI uri, String requestMethod, 339 Map<String, List<String>> requestHeaders) throws IOException { 340 requestHeadersRef.set(requestHeaders); 341 return null; 342 } 343 344 @Override 345 public CacheRequest put(URI uri, URLConnection conn) throws IOException { 346 return null; 347 } 348 }); 349 350 URL url = server.getUrl("/"); 351 URLConnection urlConnection = openConnection(url); 352 urlConnection.addRequestProperty("A", "android"); 353 readAscii(urlConnection); 354 assertEquals(Arrays.asList("android"), requestHeadersRef.get().get("A")); 355 } 356 357 @Test public void serverDisconnectsPrematurelyWithContentLengthHeader() throws IOException { 358 testServerPrematureDisconnect(TransferKind.FIXED_LENGTH); 359 } 360 361 @Test public void serverDisconnectsPrematurelyWithChunkedEncoding() throws IOException { 362 testServerPrematureDisconnect(TransferKind.CHUNKED); 363 } 364 365 @Test public void serverDisconnectsPrematurelyWithNoLengthHeaders() throws IOException { 366 // Intentionally empty. This case doesn't make sense because there's no 367 // such thing as a premature disconnect when the disconnect itself 368 // indicates the end of the data stream. 369 } 370 371 private void testServerPrematureDisconnect(TransferKind transferKind) throws IOException { 372 MockResponse response = new MockResponse(); 373 transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16); 374 server.enqueue(truncateViolently(response, 16)); 375 server.enqueue(new MockResponse().setBody("Request #2")); 376 server.play(); 377 378 BufferedReader reader = new BufferedReader( 379 new InputStreamReader(openConnection(server.getUrl("/")).getInputStream())); 380 assertEquals("ABCDE", reader.readLine()); 381 try { 382 reader.readLine(); 383 fail("This implementation silently ignored a truncated HTTP body."); 384 } catch (IOException expected) { 385 expected.printStackTrace(); 386 } finally { 387 reader.close(); 388 } 389 390 URLConnection connection = openConnection(server.getUrl("/")); 391 assertEquals("Request #2", readAscii(connection)); 392 } 393 394 @Test public void clientPrematureDisconnectWithContentLengthHeader() throws IOException { 395 testClientPrematureDisconnect(TransferKind.FIXED_LENGTH); 396 } 397 398 @Test public void clientPrematureDisconnectWithChunkedEncoding() throws IOException { 399 testClientPrematureDisconnect(TransferKind.CHUNKED); 400 } 401 402 @Test public void clientPrematureDisconnectWithNoLengthHeaders() throws IOException { 403 testClientPrematureDisconnect(TransferKind.END_OF_STREAM); 404 } 405 406 private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException { 407 // Setting a low transfer speed ensures that stream discarding will time out. 408 MockResponse response = new MockResponse().throttleBody(6, 1, TimeUnit.SECONDS); 409 transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024); 410 server.enqueue(response); 411 server.enqueue(new MockResponse().setBody("Request #2")); 412 server.play(); 413 414 URLConnection connection = openConnection(server.getUrl("/")); 415 InputStream in = connection.getInputStream(); 416 assertEquals("ABCDE", readAscii(connection, 5)); 417 in.close(); 418 try { 419 in.read(); 420 fail("Expected an IOException because the stream is closed."); 421 } catch (IOException expected) { 422 } 423 424 connection = openConnection(server.getUrl("/")); 425 assertEquals("Request #2", readAscii(connection)); 426 } 427 428 @Test public void defaultExpirationDateFullyCachedForLessThan24Hours() throws Exception { 429 // last modified: 105 seconds ago 430 // served: 5 seconds ago 431 // default lifetime: (105 - 5) / 10 = 10 seconds 432 // expires: 10 seconds from served date = 5 seconds from now 433 server.enqueue( 434 new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) 435 .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) 436 .setBody("A")); 437 server.play(); 438 439 URL url = server.getUrl("/"); 440 assertEquals("A", readAscii(openConnection(url))); 441 URLConnection connection = openConnection(url); 442 assertEquals("A", readAscii(connection)); 443 assertNull(connection.getHeaderField("Warning")); 444 } 445 446 @Test public void defaultExpirationDateConditionallyCached() throws Exception { 447 // last modified: 115 seconds ago 448 // served: 15 seconds ago 449 // default lifetime: (115 - 15) / 10 = 10 seconds 450 // expires: 10 seconds from served date = 5 seconds ago 451 String lastModifiedDate = formatDate(-115, TimeUnit.SECONDS); 452 RecordedRequest conditionalRequest = assertConditionallyCached( 453 new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) 454 .addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS))); 455 List<String> headers = conditionalRequest.getHeaders(); 456 assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); 457 } 458 459 @Test public void defaultExpirationDateFullyCachedForMoreThan24Hours() throws Exception { 460 // last modified: 105 days ago 461 // served: 5 days ago 462 // default lifetime: (105 - 5) / 10 = 10 days 463 // expires: 10 days from served date = 5 days from now 464 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.DAYS)) 465 .addHeader("Date: " + formatDate(-5, TimeUnit.DAYS)) 466 .setBody("A")); 467 server.play(); 468 469 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 470 URLConnection connection = openConnection(server.getUrl("/")); 471 assertEquals("A", readAscii(connection)); 472 assertEquals("113 HttpURLConnection \"Heuristic expiration\"", 473 connection.getHeaderField("Warning")); 474 } 475 476 @Test public void noDefaultExpirationForUrlsWithQueryString() throws Exception { 477 server.enqueue( 478 new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) 479 .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) 480 .setBody("A")); 481 server.enqueue(new MockResponse().setBody("B")); 482 server.play(); 483 484 URL url = server.getUrl("/?foo=bar"); 485 assertEquals("A", readAscii(openConnection(url))); 486 assertEquals("B", readAscii(openConnection(url))); 487 } 488 489 @Test public void expirationDateInThePastWithLastModifiedHeader() throws Exception { 490 String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); 491 RecordedRequest conditionalRequest = assertConditionallyCached( 492 new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) 493 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); 494 List<String> headers = conditionalRequest.getHeaders(); 495 assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); 496 } 497 498 @Test public void expirationDateInThePastWithNoLastModifiedHeader() throws Exception { 499 assertNotCached(new MockResponse().addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); 500 } 501 502 @Test public void expirationDateInTheFuture() throws Exception { 503 assertFullyCached(new MockResponse().addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); 504 } 505 506 @Test public void maxAgePreferredWithMaxAgeAndExpires() throws Exception { 507 assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) 508 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) 509 .addHeader("Cache-Control: max-age=60")); 510 } 511 512 @Test public void maxAgeInThePastWithDateAndLastModifiedHeaders() throws Exception { 513 String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); 514 RecordedRequest conditionalRequest = assertConditionallyCached( 515 new MockResponse().addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) 516 .addHeader("Last-Modified: " + lastModifiedDate) 517 .addHeader("Cache-Control: max-age=60")); 518 List<String> headers = conditionalRequest.getHeaders(); 519 assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); 520 } 521 522 @Test public void maxAgeInThePastWithDateHeaderButNoLastModifiedHeader() throws Exception { 523 // Chrome interprets max-age relative to the local clock. Both our cache 524 // and Firefox both use the earlier of the local and server's clock. 525 assertNotCached(new MockResponse().addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) 526 .addHeader("Cache-Control: max-age=60")); 527 } 528 529 @Test public void maxAgeInTheFutureWithDateHeader() throws Exception { 530 assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) 531 .addHeader("Cache-Control: max-age=60")); 532 } 533 534 @Test public void maxAgeInTheFutureWithNoDateHeader() throws Exception { 535 assertFullyCached(new MockResponse().addHeader("Cache-Control: max-age=60")); 536 } 537 538 @Test public void maxAgeWithLastModifiedButNoServedDate() throws Exception { 539 assertFullyCached( 540 new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) 541 .addHeader("Cache-Control: max-age=60")); 542 } 543 544 @Test public void maxAgeInTheFutureWithDateAndLastModifiedHeaders() throws Exception { 545 assertFullyCached( 546 new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) 547 .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) 548 .addHeader("Cache-Control: max-age=60")); 549 } 550 551 @Test public void maxAgePreferredOverLowerSharedMaxAge() throws Exception { 552 assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) 553 .addHeader("Cache-Control: s-maxage=60") 554 .addHeader("Cache-Control: max-age=180")); 555 } 556 557 @Test public void maxAgePreferredOverHigherMaxAge() throws Exception { 558 assertNotCached(new MockResponse().addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) 559 .addHeader("Cache-Control: s-maxage=180") 560 .addHeader("Cache-Control: max-age=60")); 561 } 562 563 /** 564 * Tests that the ResponseCache can cache something. The InMemoryResponseCache only caches GET 565 * requests. 566 */ 567 @Test public void responseCacheCanCache() throws Exception { 568 testRequestMethod("GET", true); 569 } 570 571 /** 572 * Confirm the ResponseCache can elect to not cache something. The InMemoryResponseCache only 573 * caches GET requests. 574 */ 575 @Test public void responseCacheCanIgnore() throws Exception { 576 testRequestMethod("HEAD", false); 577 } 578 579 private void testRequestMethod(String requestMethod, boolean expectCached) throws Exception { 580 // 1. seed the cache (potentially) 581 // 2. expect a cache hit or miss 582 server.enqueue(new MockResponse().addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 583 .addHeader("X-Response-ID: 1")); 584 server.enqueue(new MockResponse().addHeader("X-Response-ID: 2")); 585 server.play(); 586 587 URL url = server.getUrl("/"); 588 589 HttpURLConnection request1 = openConnection(url); 590 request1.setRequestMethod(requestMethod); 591 addRequestBodyIfNecessary(requestMethod, request1); 592 assertEquals("1", request1.getHeaderField("X-Response-ID")); 593 594 URLConnection request2 = openConnection(url); 595 if (expectCached) { 596 assertEquals("1", request2.getHeaderField("X-Response-ID")); 597 } else { 598 assertEquals("2", request2.getHeaderField("X-Response-ID")); 599 } 600 } 601 602 /** 603 * Equivalent to {@link HttpResponseCacheTest#postInvalidatesCacheWithUncacheableResponse()} but 604 * demonstrating that {@link ResponseCache} provides no mechanism for cache invalidation as the 605 * result of locally-made requests. In reality invalidation could take place from other clients at 606 * any time. 607 */ 608 @Test public void postInvalidatesCacheWithUncacheableResponse() throws Exception { 609 // 1. seed the cache 610 // 2. invalidate it with uncacheable response 611 // 3. the cache to return the original value 612 server.enqueue( 613 new MockResponse().setBody("A").addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); 614 server.enqueue(new MockResponse().setBody("B").setResponseCode(500)); 615 server.play(); 616 617 URL url = server.getUrl("/"); 618 619 assertEquals("A", readAscii(openConnection(url))); 620 621 HttpURLConnection invalidate = openConnection(url); 622 invalidate.setRequestMethod("POST"); 623 addRequestBodyIfNecessary("POST", invalidate); 624 assertEquals("B", readAscii(invalidate)); 625 626 assertEquals("A", readAscii(openConnection(url))); 627 } 628 629 @Test public void etag() throws Exception { 630 RecordedRequest conditionalRequest = 631 assertConditionallyCached(new MockResponse().addHeader("ETag: v1")); 632 assertTrue(conditionalRequest.getHeaders().contains("If-None-Match: v1")); 633 } 634 635 @Test public void etagAndExpirationDateInThePast() throws Exception { 636 String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); 637 RecordedRequest conditionalRequest = assertConditionallyCached( 638 new MockResponse().addHeader("ETag: v1") 639 .addHeader("Last-Modified: " + lastModifiedDate) 640 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); 641 List<String> headers = conditionalRequest.getHeaders(); 642 assertTrue(headers.contains("If-None-Match: v1")); 643 assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); 644 } 645 646 @Test public void etagAndExpirationDateInTheFuture() throws Exception { 647 assertFullyCached(new MockResponse().addHeader("ETag: v1") 648 .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) 649 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); 650 } 651 652 @Test public void cacheControlNoCache() throws Exception { 653 assertNotCached(new MockResponse().addHeader("Cache-Control: no-cache")); 654 } 655 656 @Test public void cacheControlNoCacheAndExpirationDateInTheFuture() throws Exception { 657 String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); 658 RecordedRequest conditionalRequest = assertConditionallyCached( 659 new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) 660 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 661 .addHeader("Cache-Control: no-cache")); 662 List<String> headers = conditionalRequest.getHeaders(); 663 assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); 664 } 665 666 @Test public void pragmaNoCache() throws Exception { 667 assertNotCached(new MockResponse().addHeader("Pragma: no-cache")); 668 } 669 670 @Test public void pragmaNoCacheAndExpirationDateInTheFuture() throws Exception { 671 String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); 672 RecordedRequest conditionalRequest = assertConditionallyCached( 673 new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) 674 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 675 .addHeader("Pragma: no-cache")); 676 List<String> headers = conditionalRequest.getHeaders(); 677 assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); 678 } 679 680 @Test public void cacheControlNoStore() throws Exception { 681 assertNotCached(new MockResponse().addHeader("Cache-Control: no-store")); 682 } 683 684 @Test public void cacheControlNoStoreAndExpirationDateInTheFuture() throws Exception { 685 assertNotCached(new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) 686 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 687 .addHeader("Cache-Control: no-store")); 688 } 689 690 @Test public void partialRangeResponsesDoNotCorruptCache() throws Exception { 691 // 1. request a range 692 // 2. request a full document, expecting a cache miss 693 server.enqueue(new MockResponse().setBody("AA") 694 .setResponseCode(HttpURLConnection.HTTP_PARTIAL) 695 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 696 .addHeader("Content-Range: bytes 1000-1001/2000")); 697 server.enqueue(new MockResponse().setBody("BB")); 698 server.play(); 699 700 URL url = server.getUrl("/"); 701 702 URLConnection range = openConnection(url); 703 range.addRequestProperty("Range", "bytes=1000-1001"); 704 assertEquals("AA", readAscii(range)); 705 706 assertEquals("BB", readAscii(openConnection(url))); 707 } 708 709 @Test public void serverReturnsDocumentOlderThanCache() throws Exception { 710 server.enqueue(new MockResponse().setBody("A") 711 .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) 712 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); 713 server.enqueue(new MockResponse().setBody("B") 714 .addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS))); 715 server.play(); 716 717 URL url = server.getUrl("/"); 718 719 assertEquals("A", readAscii(openConnection(url))); 720 assertEquals("A", readAscii(openConnection(url))); 721 } 722 723 @Test public void nonIdentityEncodingAndConditionalCache() throws Exception { 724 assertNonIdentityEncodingCached( 725 new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) 726 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); 727 } 728 729 @Test public void nonIdentityEncodingAndFullCache() throws Exception { 730 assertNonIdentityEncodingCached( 731 new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) 732 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); 733 } 734 735 private void assertNonIdentityEncodingCached(MockResponse response) throws Exception { 736 server.enqueue( 737 response.setBody(gzip("ABCABCABC".getBytes("UTF-8"))).addHeader("Content-Encoding: gzip")); 738 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 739 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 740 741 server.play(); 742 743 // At least three request/response pairs are required because after the first request is cached 744 // a different execution path might be taken. Thus modifications to the cache applied during 745 // the second request might not be visible until another request is performed. 746 assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/")))); 747 assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/")))); 748 assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/")))); 749 } 750 751 @Test public void notModifiedSpecifiesEncoding() throws Exception { 752 server.enqueue(new MockResponse() 753 .setBody(gzip("ABCABCABC".getBytes("UTF-8"))) 754 .addHeader("Content-Encoding: gzip") 755 .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) 756 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); 757 server.enqueue(new MockResponse() 758 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED) 759 .addHeader("Content-Encoding: gzip")); 760 server.enqueue(new MockResponse() 761 .setBody("DEFDEFDEF")); 762 763 server.play(); 764 assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/")))); 765 assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/")))); 766 assertEquals("DEFDEFDEF", readAscii(openConnection(server.getUrl("/")))); 767 } 768 769 @Test public void expiresDateBeforeModifiedDate() throws Exception { 770 assertConditionallyCached( 771 new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 772 .addHeader("Expires: " + formatDate(-2, TimeUnit.HOURS))); 773 } 774 775 @Test public void requestMaxAge() throws IOException { 776 server.enqueue(new MockResponse().setBody("A") 777 .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) 778 .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)) 779 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); 780 server.enqueue(new MockResponse().setBody("B")); 781 782 server.play(); 783 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 784 785 URLConnection connection = openConnection(server.getUrl("/")); 786 connection.addRequestProperty("Cache-Control", "max-age=30"); 787 assertEquals("B", readAscii(connection)); 788 } 789 790 @Test public void requestMinFresh() throws IOException { 791 server.enqueue(new MockResponse().setBody("A") 792 .addHeader("Cache-Control: max-age=60") 793 .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); 794 server.enqueue(new MockResponse().setBody("B")); 795 796 server.play(); 797 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 798 799 URLConnection connection = openConnection(server.getUrl("/")); 800 connection.addRequestProperty("Cache-Control", "min-fresh=120"); 801 assertEquals("B", readAscii(connection)); 802 } 803 804 @Test public void requestMaxStale() throws IOException { 805 server.enqueue(new MockResponse().setBody("A") 806 .addHeader("Cache-Control: max-age=120") 807 .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES))); 808 server.enqueue(new MockResponse().setBody("B")); 809 810 server.play(); 811 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 812 813 URLConnection connection = openConnection(server.getUrl("/")); 814 connection.addRequestProperty("Cache-Control", "max-stale=180"); 815 assertEquals("A", readAscii(connection)); 816 assertEquals("110 HttpURLConnection \"Response is stale\"", 817 connection.getHeaderField("Warning")); 818 } 819 820 @Test public void requestMaxStaleNotHonoredWithMustRevalidate() throws IOException { 821 server.enqueue(new MockResponse().setBody("A") 822 .addHeader("Cache-Control: max-age=120, must-revalidate") 823 .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES))); 824 server.enqueue(new MockResponse().setBody("B")); 825 826 server.play(); 827 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 828 829 URLConnection connection = openConnection(server.getUrl("/")); 830 connection.addRequestProperty("Cache-Control", "max-stale=180"); 831 assertEquals("B", readAscii(connection)); 832 } 833 834 @Test public void requestOnlyIfCachedWithNoResponseCached() throws IOException { 835 // (no responses enqueued) 836 server.play(); 837 838 HttpURLConnection connection = openConnection(server.getUrl("/")); 839 connection.addRequestProperty("Cache-Control", "only-if-cached"); 840 assertGatewayTimeout(connection); 841 } 842 843 @Test public void requestOnlyIfCachedWithFullResponseCached() throws IOException { 844 server.enqueue(new MockResponse().setBody("A") 845 .addHeader("Cache-Control: max-age=30") 846 .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); 847 server.play(); 848 849 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 850 URLConnection connection = openConnection(server.getUrl("/")); 851 connection.addRequestProperty("Cache-Control", "only-if-cached"); 852 assertEquals("A", readAscii(connection)); 853 } 854 855 @Test public void requestOnlyIfCachedWithConditionalResponseCached() throws IOException { 856 server.enqueue(new MockResponse().setBody("A") 857 .addHeader("Cache-Control: max-age=30") 858 .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES))); 859 server.play(); 860 861 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 862 HttpURLConnection connection = openConnection(server.getUrl("/")); 863 connection.addRequestProperty("Cache-Control", "only-if-cached"); 864 assertGatewayTimeout(connection); 865 } 866 867 @Test public void requestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException { 868 server.enqueue(new MockResponse().setBody("A")); 869 server.play(); 870 871 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 872 HttpURLConnection connection = openConnection(server.getUrl("/")); 873 connection.addRequestProperty("Cache-Control", "only-if-cached"); 874 assertGatewayTimeout(connection); 875 } 876 877 @Test public void requestCacheControlNoCache() throws Exception { 878 server.enqueue( 879 new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) 880 .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) 881 .addHeader("Cache-Control: max-age=60") 882 .setBody("A")); 883 server.enqueue(new MockResponse().setBody("B")); 884 server.play(); 885 886 URL url = server.getUrl("/"); 887 assertEquals("A", readAscii(openConnection(url))); 888 URLConnection connection = openConnection(url); 889 connection.setRequestProperty("Cache-Control", "no-cache"); 890 assertEquals("B", readAscii(connection)); 891 } 892 893 @Test public void requestPragmaNoCache() throws Exception { 894 server.enqueue( 895 new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) 896 .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) 897 .addHeader("Cache-Control: max-age=60") 898 .setBody("A")); 899 server.enqueue(new MockResponse().setBody("B")); 900 server.play(); 901 902 URL url = server.getUrl("/"); 903 assertEquals("A", readAscii(openConnection(url))); 904 URLConnection connection = openConnection(url); 905 connection.setRequestProperty("Pragma", "no-cache"); 906 assertEquals("B", readAscii(connection)); 907 } 908 909 @Test public void clientSuppliedIfModifiedSinceWithCachedResult() throws Exception { 910 MockResponse response = 911 new MockResponse().addHeader("ETag: v3").addHeader("Cache-Control: max-age=0"); 912 String ifModifiedSinceDate = formatDate(-24, TimeUnit.HOURS); 913 RecordedRequest request = 914 assertClientSuppliedCondition(response, "If-Modified-Since", ifModifiedSinceDate); 915 List<String> headers = request.getHeaders(); 916 assertTrue(headers.contains("If-Modified-Since: " + ifModifiedSinceDate)); 917 assertFalse(headers.contains("If-None-Match: v3")); 918 } 919 920 @Test public void clientSuppliedIfNoneMatchSinceWithCachedResult() throws Exception { 921 String lastModifiedDate = formatDate(-3, TimeUnit.MINUTES); 922 MockResponse response = new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) 923 .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) 924 .addHeader("Cache-Control: max-age=0"); 925 RecordedRequest request = assertClientSuppliedCondition(response, "If-None-Match", "v1"); 926 List<String> headers = request.getHeaders(); 927 assertTrue(headers.contains("If-None-Match: v1")); 928 assertFalse(headers.contains("If-Modified-Since: " + lastModifiedDate)); 929 } 930 931 private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String conditionName, 932 String conditionValue) throws Exception { 933 server.enqueue(seed.setBody("A")); 934 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 935 server.play(); 936 937 URL url = server.getUrl("/"); 938 assertEquals("A", readAscii(openConnection(url))); 939 940 HttpURLConnection connection = openConnection(url); 941 connection.addRequestProperty(conditionName, conditionValue); 942 assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); 943 assertEquals("", readAscii(connection)); 944 945 server.takeRequest(); // seed 946 return server.takeRequest(); 947 } 948 949 @Test public void setIfModifiedSince() throws Exception { 950 Date since = new Date(); 951 server.enqueue(new MockResponse().setBody("A")); 952 server.play(); 953 954 URL url = server.getUrl("/"); 955 URLConnection connection = openConnection(url); 956 connection.setIfModifiedSince(since.getTime()); 957 assertEquals("A", readAscii(connection)); 958 RecordedRequest request = server.takeRequest(); 959 assertTrue(request.getHeaders().contains("If-Modified-Since: " + formatDate(since))); 960 } 961 962 @Test public void clientSuppliedConditionWithoutCachedResult() throws Exception { 963 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 964 server.play(); 965 966 HttpURLConnection connection = openConnection(server.getUrl("/")); 967 String clientIfModifiedSince = formatDate(-24, TimeUnit.HOURS); 968 connection.addRequestProperty("If-Modified-Since", clientIfModifiedSince); 969 assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); 970 assertEquals("", readAscii(connection)); 971 } 972 973 @Test public void authorizationRequestHeaderPreventsCaching() throws Exception { 974 server.enqueue( 975 new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.MINUTES)) 976 .addHeader("Cache-Control: max-age=60") 977 .setBody("A")); 978 server.enqueue(new MockResponse().setBody("B")); 979 server.play(); 980 981 URL url = server.getUrl("/"); 982 URLConnection connection = openConnection(url); 983 connection.addRequestProperty("Authorization", "password"); 984 assertEquals("A", readAscii(connection)); 985 assertEquals("B", readAscii(openConnection(url))); 986 } 987 988 @Test public void authorizationResponseCachedWithSMaxAge() throws Exception { 989 assertAuthorizationRequestFullyCached( 990 new MockResponse().addHeader("Cache-Control: s-maxage=60")); 991 } 992 993 @Test public void authorizationResponseCachedWithPublic() throws Exception { 994 assertAuthorizationRequestFullyCached(new MockResponse().addHeader("Cache-Control: public")); 995 } 996 997 @Test public void authorizationResponseCachedWithMustRevalidate() throws Exception { 998 assertAuthorizationRequestFullyCached( 999 new MockResponse().addHeader("Cache-Control: must-revalidate")); 1000 } 1001 1002 public void assertAuthorizationRequestFullyCached(MockResponse response) throws Exception { 1003 server.enqueue(response.addHeader("Cache-Control: max-age=60").setBody("A")); 1004 server.enqueue(new MockResponse().setBody("B")); 1005 server.play(); 1006 1007 URL url = server.getUrl("/"); 1008 URLConnection connection = openConnection(url); 1009 connection.addRequestProperty("Authorization", "password"); 1010 assertEquals("A", readAscii(connection)); 1011 assertEquals("A", readAscii(openConnection(url))); 1012 } 1013 1014 @Test public void contentLocationDoesNotPopulateCache() throws Exception { 1015 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") 1016 .addHeader("Content-Location: /bar") 1017 .setBody("A")); 1018 server.enqueue(new MockResponse().setBody("B")); 1019 server.play(); 1020 1021 assertEquals("A", readAscii(openConnection(server.getUrl("/foo")))); 1022 assertEquals("B", readAscii(openConnection(server.getUrl("/bar")))); 1023 } 1024 1025 @Test public void useCachesFalseDoesNotWriteToCache() throws Exception { 1026 server.enqueue( 1027 new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A")); 1028 server.enqueue(new MockResponse().setBody("B")); 1029 server.play(); 1030 1031 URLConnection connection = openConnection(server.getUrl("/")); 1032 connection.setUseCaches(false); 1033 assertEquals("A", readAscii(connection)); 1034 assertEquals("B", readAscii(openConnection(server.getUrl("/")))); 1035 } 1036 1037 @Test public void useCachesFalseDoesNotReadFromCache() throws Exception { 1038 server.enqueue( 1039 new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A")); 1040 server.enqueue(new MockResponse().setBody("B")); 1041 server.play(); 1042 1043 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1044 URLConnection connection = openConnection(server.getUrl("/")); 1045 connection.setUseCaches(false); 1046 assertEquals("B", readAscii(connection)); 1047 } 1048 1049 @Test public void defaultUseCachesSetsInitialValueOnly() throws Exception { 1050 URL url = new URL("http://localhost/"); 1051 URLConnection c1 = openConnection(url); 1052 URLConnection c2 = openConnection(url); 1053 assertTrue(c1.getDefaultUseCaches()); 1054 c1.setDefaultUseCaches(false); 1055 try { 1056 assertTrue(c1.getUseCaches()); 1057 assertTrue(c2.getUseCaches()); 1058 URLConnection c3 = openConnection(url); 1059 assertFalse(c3.getUseCaches()); 1060 } finally { 1061 c1.setDefaultUseCaches(true); 1062 } 1063 } 1064 1065 @Test public void connectionIsReturnedToPoolAfterConditionalSuccess() throws Exception { 1066 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 1067 .addHeader("Cache-Control: max-age=0") 1068 .setBody("A")); 1069 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1070 server.enqueue(new MockResponse().setBody("B")); 1071 server.play(); 1072 1073 assertEquals("A", readAscii(openConnection(server.getUrl("/a")))); 1074 assertEquals("A", readAscii(openConnection(server.getUrl("/a")))); 1075 assertEquals("B", readAscii(openConnection(server.getUrl("/b")))); 1076 1077 assertEquals(0, server.takeRequest().getSequenceNumber()); 1078 assertEquals(1, server.takeRequest().getSequenceNumber()); 1079 assertEquals(2, server.takeRequest().getSequenceNumber()); 1080 } 1081 1082 /** 1083 * Confirms the cache implementation may determine the criteria for caching. In real caches 1084 * this would be the "Vary" headers. 1085 */ 1086 @Test public void cacheCanUseCriteriaBesidesVariantObeyed() throws Exception { 1087 server.enqueue( 1088 new MockResponse().addHeader("Cache-Control: max-age=60") 1089 .addHeader(InMemoryResponseCache.CACHE_VARIANT_HEADER, "A").setBody("A")); 1090 server.enqueue( 1091 new MockResponse().addHeader("Cache-Control: max-age=60") 1092 .addHeader(InMemoryResponseCache.CACHE_VARIANT_HEADER, "B").setBody("B")); 1093 server.play(); 1094 1095 URL url = server.getUrl("/"); 1096 URLConnection connection1 = openConnection(url); 1097 connection1.addRequestProperty(InMemoryResponseCache.CACHE_VARIANT_HEADER, "A"); 1098 assertEquals("A", readAscii(connection1)); 1099 URLConnection connection2 = openConnection(url); 1100 connection2.addRequestProperty(InMemoryResponseCache.CACHE_VARIANT_HEADER, "A"); 1101 assertEquals("A", readAscii(connection2)); 1102 assertEquals(1, server.getRequestCount()); 1103 1104 URLConnection connection3 = openConnection(url); 1105 connection3.addRequestProperty(InMemoryResponseCache.CACHE_VARIANT_HEADER, "B"); 1106 assertEquals("B", readAscii(connection3)); 1107 assertEquals(2, server.getRequestCount()); 1108 1109 URLConnection connection4 = openConnection(url); 1110 connection4.addRequestProperty(InMemoryResponseCache.CACHE_VARIANT_HEADER, "A"); 1111 assertEquals("A", readAscii(connection4)); 1112 assertEquals(2, server.getRequestCount()); 1113 } 1114 1115 @Test public void cachePlusCookies() throws Exception { 1116 server.enqueue(new MockResponse().addHeader( 1117 "Set-Cookie: a=FIRST; domain=" + server.getCookieDomain() + ";") 1118 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 1119 .addHeader("Cache-Control: max-age=0") 1120 .setBody("A")); 1121 server.enqueue(new MockResponse().addHeader( 1122 "Set-Cookie: a=SECOND; domain=" + server.getCookieDomain() + ";") 1123 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1124 server.play(); 1125 1126 CookieManager cookieManager = new CookieManager(); 1127 CookieManager.setDefault(cookieManager); 1128 1129 URL url = server.getUrl("/"); 1130 assertEquals("A", readAscii(openConnection(url))); 1131 assertCookies(cookieManager, url, "a=FIRST"); 1132 assertEquals("A", readAscii(openConnection(url))); 1133 assertCookies(cookieManager, url, "a=SECOND"); 1134 } 1135 1136 @Test public void getHeadersReturnsNetworkEndToEndHeaders() throws Exception { 1137 server.enqueue(new MockResponse().addHeader("Allow: GET, HEAD") 1138 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 1139 .addHeader("Cache-Control: max-age=0") 1140 .setBody("A")); 1141 server.enqueue(new MockResponse().addHeader("Allow: GET, HEAD, PUT") 1142 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1143 server.play(); 1144 1145 URLConnection connection1 = openConnection(server.getUrl("/")); 1146 assertEquals("A", readAscii(connection1)); 1147 assertEquals("GET, HEAD", connection1.getHeaderField("Allow")); 1148 1149 URLConnection connection2 = openConnection(server.getUrl("/")); 1150 assertEquals("A", readAscii(connection2)); 1151 assertEquals("GET, HEAD, PUT", connection2.getHeaderField("Allow")); 1152 } 1153 1154 @Test public void getHeadersReturnsCachedHopByHopHeaders() throws Exception { 1155 server.enqueue(new MockResponse().addHeader("Transfer-Encoding: identity") 1156 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 1157 .addHeader("Cache-Control: max-age=0") 1158 .setBody("A")); 1159 server.enqueue(new MockResponse().addHeader("Transfer-Encoding: none") 1160 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1161 server.play(); 1162 1163 URLConnection connection1 = openConnection(server.getUrl("/")); 1164 assertEquals("A", readAscii(connection1)); 1165 assertEquals("identity", connection1.getHeaderField("Transfer-Encoding")); 1166 1167 URLConnection connection2 = openConnection(server.getUrl("/")); 1168 assertEquals("A", readAscii(connection2)); 1169 assertEquals("identity", connection2.getHeaderField("Transfer-Encoding")); 1170 } 1171 1172 @Test public void getHeadersDeletesCached100LevelWarnings() throws Exception { 1173 server.enqueue(new MockResponse().addHeader("Warning: 199 test danger") 1174 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 1175 .addHeader("Cache-Control: max-age=0") 1176 .setBody("A")); 1177 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1178 server.play(); 1179 1180 URLConnection connection1 = openConnection(server.getUrl("/")); 1181 assertEquals("A", readAscii(connection1)); 1182 assertEquals("199 test danger", connection1.getHeaderField("Warning")); 1183 1184 URLConnection connection2 = openConnection(server.getUrl("/")); 1185 assertEquals("A", readAscii(connection2)); 1186 assertEquals(null, connection2.getHeaderField("Warning")); 1187 } 1188 1189 @Test public void getHeadersRetainsCached200LevelWarnings() throws Exception { 1190 server.enqueue(new MockResponse().addHeader("Warning: 299 test danger") 1191 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 1192 .addHeader("Cache-Control: max-age=0") 1193 .setBody("A")); 1194 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1195 server.play(); 1196 1197 URLConnection connection1 = openConnection(server.getUrl("/")); 1198 assertEquals("A", readAscii(connection1)); 1199 assertEquals("299 test danger", connection1.getHeaderField("Warning")); 1200 1201 URLConnection connection2 = openConnection(server.getUrl("/")); 1202 assertEquals("A", readAscii(connection2)); 1203 assertEquals("299 test danger", connection2.getHeaderField("Warning")); 1204 } 1205 1206 public void assertCookies(CookieManager cookieManager, URL url, String... expectedCookies) 1207 throws Exception { 1208 List<String> actualCookies = new ArrayList<String>(); 1209 for (HttpCookie cookie : cookieManager.getCookieStore().get(url.toURI())) { 1210 actualCookies.add(cookie.toString()); 1211 } 1212 assertEquals(Arrays.asList(expectedCookies), actualCookies); 1213 } 1214 1215 @Test public void cachePlusRange() throws Exception { 1216 assertNotCached(new MockResponse().setResponseCode(HttpURLConnection.HTTP_PARTIAL) 1217 .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) 1218 .addHeader("Content-Range: bytes 100-100/200") 1219 .addHeader("Cache-Control: max-age=60")); 1220 } 1221 1222 /** 1223 * Equivalent to {@link HttpResponseCacheTest#conditionalHitUpdatesCache()}, except a Java 1224 * standard cache has no means to update the headers for an existing entry so the behavior is 1225 * different. 1226 */ 1227 @Test public void conditionalHitDoesNotUpdateCache() throws Exception { 1228 // A response that is cacheable, but with a short life. 1229 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(0, TimeUnit.SECONDS)) 1230 .addHeader("Cache-Control: max-age=0") 1231 .setBody("A")); 1232 // A response that refers to the previous response, but is cacheable with a long life. 1233 // Contains a header we can recognize as having come from the server. 1234 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=30") 1235 .addHeader("Allow: GET, HEAD") 1236 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1237 // A response that is cacheable with a long life. 1238 server.enqueue(new MockResponse().setBody("B").addHeader("Cache-Control: max-age=30")); 1239 // A response that should never be requested. 1240 server.enqueue(new MockResponse().setBody("C")); 1241 server.play(); 1242 1243 // cache miss; seed the cache with an entry that will require a network hit to be sure it is 1244 // still valid 1245 HttpURLConnection connection1 = openConnection(server.getUrl("/a")); 1246 assertEquals("A", readAscii(connection1)); 1247 assertEquals(null, connection1.getHeaderField("Allow")); 1248 1249 // conditional cache hit; The cached data should be returned, but the cache is not updated. 1250 HttpURLConnection connection2 = openConnection(server.getUrl("/a")); 1251 assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); 1252 assertEquals("A", readAscii(connection2)); 1253 assertEquals("GET, HEAD", connection2.getHeaderField("Allow")); 1254 1255 // conditional cache hit; The server responds with new data. The cache is updated. 1256 HttpURLConnection connection3 = openConnection(server.getUrl("/a")); 1257 assertEquals("B", readAscii(connection3)); 1258 1259 // full cache hit; The data from connection3 has now replaced that from connection 1. 1260 HttpURLConnection connection4 = openConnection(server.getUrl("/a")); 1261 assertEquals("B", readAscii(connection4)); 1262 1263 assertEquals(3, server.getRequestCount()); 1264 } 1265 1266 @Test public void responseSourceHeaderCached() throws IOException { 1267 server.enqueue(new MockResponse().setBody("A") 1268 .addHeader("Cache-Control: max-age=30") 1269 .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); 1270 server.play(); 1271 1272 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1273 URLConnection connection = openConnection(server.getUrl("/")); 1274 connection.addRequestProperty("Cache-Control", "only-if-cached"); 1275 assertEquals("A", readAscii(connection)); 1276 1277 String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE); 1278 assertEquals(ResponseSource.CACHE + " 200", source); 1279 } 1280 1281 @Test public void responseSourceHeaderConditionalCacheFetched() throws IOException { 1282 server.enqueue(new MockResponse().setBody("A") 1283 .addHeader("Cache-Control: max-age=30") 1284 .addHeader("Date: " + formatDate(-31, TimeUnit.MINUTES))); 1285 server.enqueue(new MockResponse().setBody("B") 1286 .addHeader("Cache-Control: max-age=30") 1287 .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); 1288 server.play(); 1289 1290 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1291 HttpURLConnection connection = openConnection(server.getUrl("/")); 1292 assertEquals("B", readAscii(connection)); 1293 1294 String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE); 1295 assertEquals(ResponseSource.CONDITIONAL_CACHE + " 200", source); 1296 } 1297 1298 @Test public void responseSourceHeaderConditionalCacheNotFetched() throws IOException { 1299 server.enqueue(new MockResponse().setBody("A") 1300 .addHeader("Cache-Control: max-age=0") 1301 .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); 1302 server.enqueue(new MockResponse().setResponseCode(304)); 1303 server.play(); 1304 1305 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1306 HttpURLConnection connection = openConnection(server.getUrl("/")); 1307 assertEquals("A", readAscii(connection)); 1308 1309 String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE); 1310 assertEquals(ResponseSource.CONDITIONAL_CACHE + " 304", source); 1311 } 1312 1313 @Test public void responseSourceHeaderFetched() throws IOException { 1314 server.enqueue(new MockResponse().setBody("A")); 1315 server.play(); 1316 1317 URLConnection connection = openConnection(server.getUrl("/")); 1318 assertEquals("A", readAscii(connection)); 1319 1320 String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE); 1321 assertEquals(ResponseSource.NETWORK + " 200", source); 1322 } 1323 1324 @Test public void emptyResponseHeaderNameFromCacheIsLenient() throws Exception { 1325 server.enqueue(new MockResponse() 1326 .addHeader("Cache-Control: max-age=120") 1327 .addHeader(": A") 1328 .setBody("body")); 1329 server.play(); 1330 HttpURLConnection connection = openConnection(server.getUrl("/")); 1331 assertEquals("A", connection.getHeaderField("")); 1332 } 1333 1334 /** 1335 * @param delta the offset from the current date to use. Negative 1336 * values yield dates in the past; positive values yield dates in the 1337 * future. 1338 */ 1339 private String formatDate(long delta, TimeUnit timeUnit) { 1340 return formatDate(new Date(System.currentTimeMillis() + timeUnit.toMillis(delta))); 1341 } 1342 1343 private String formatDate(Date date) { 1344 DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); 1345 rfc1123.setTimeZone(TimeZone.getTimeZone("GMT")); 1346 return rfc1123.format(date); 1347 } 1348 1349 private void addRequestBodyIfNecessary(String requestMethod, HttpURLConnection invalidate) 1350 throws IOException { 1351 if (requestMethod.equals("POST") || requestMethod.equals("PUT")) { 1352 invalidate.setDoOutput(true); 1353 OutputStream requestBody = invalidate.getOutputStream(); 1354 requestBody.write('x'); 1355 requestBody.close(); 1356 } 1357 } 1358 1359 private void assertNotCached(MockResponse response) throws Exception { 1360 server.enqueue(response.setBody("A")); 1361 server.enqueue(new MockResponse().setBody("B")); 1362 server.play(); 1363 1364 URL url = server.getUrl("/"); 1365 assertEquals("A", readAscii(openConnection(url))); 1366 assertEquals("B", readAscii(openConnection(url))); 1367 } 1368 1369 /** @return the request with the conditional get headers. */ 1370 private RecordedRequest assertConditionallyCached(MockResponse response) throws Exception { 1371 // scenario 1: condition succeeds 1372 server.enqueue(response.setBody("A").setStatus("HTTP/1.1 200 A-OK")); 1373 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1374 1375 // scenario 2: condition fails 1376 server.enqueue(response.setBody("B").setStatus("HTTP/1.1 200 B-OK")); 1377 server.enqueue(new MockResponse().setStatus("HTTP/1.1 200 C-OK").setBody("C")); 1378 1379 server.play(); 1380 1381 URL valid = server.getUrl("/valid"); 1382 HttpURLConnection connection1 = openConnection(valid); 1383 assertEquals("A", readAscii(connection1)); 1384 assertEquals(HttpURLConnection.HTTP_OK, connection1.getResponseCode()); 1385 assertEquals("A-OK", connection1.getResponseMessage()); 1386 HttpURLConnection connection2 = openConnection(valid); 1387 assertEquals("A", readAscii(connection2)); 1388 assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); 1389 assertEquals("A-OK", connection2.getResponseMessage()); 1390 1391 URL invalid = server.getUrl("/invalid"); 1392 HttpURLConnection connection3 = openConnection(invalid); 1393 assertEquals("B", readAscii(connection3)); 1394 assertEquals(HttpURLConnection.HTTP_OK, connection3.getResponseCode()); 1395 assertEquals("B-OK", connection3.getResponseMessage()); 1396 HttpURLConnection connection4 = openConnection(invalid); 1397 assertEquals("C", readAscii(connection4)); 1398 assertEquals(HttpURLConnection.HTTP_OK, connection4.getResponseCode()); 1399 assertEquals("C-OK", connection4.getResponseMessage()); 1400 1401 server.takeRequest(); // regular get 1402 return server.takeRequest(); // conditional get 1403 } 1404 1405 private void assertFullyCached(MockResponse response) throws Exception { 1406 server.enqueue(response.setBody("A")); 1407 server.enqueue(response.setBody("B")); 1408 server.play(); 1409 1410 URL url = server.getUrl("/"); 1411 assertEquals("A", readAscii(openConnection(url))); 1412 assertEquals("A", readAscii(openConnection(url))); 1413 } 1414 1415 /** 1416 * Shortens the body of {@code response} but not the corresponding headers. 1417 * Only useful to test how clients respond to the premature conclusion of 1418 * the HTTP body. 1419 */ 1420 private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) { 1421 response.setSocketPolicy(DISCONNECT_AT_END); 1422 List<String> headers = new ArrayList<String>(response.getHeaders()); 1423 response.setBody(Arrays.copyOfRange(response.getBody(), 0, numBytesToKeep)); 1424 response.getHeaders().clear(); 1425 response.getHeaders().addAll(headers); 1426 return response; 1427 } 1428 1429 /** 1430 * Reads {@code count} characters from the stream. If the stream is 1431 * exhausted before {@code count} characters can be read, the remaining 1432 * characters are returned and the stream is closed. 1433 */ 1434 private String readAscii(URLConnection connection, int count) throws IOException { 1435 HttpURLConnection httpConnection = (HttpURLConnection) connection; 1436 InputStream in = httpConnection.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST 1437 ? connection.getInputStream() : httpConnection.getErrorStream(); 1438 StringBuilder result = new StringBuilder(); 1439 for (int i = 0; i < count; i++) { 1440 int value = in.read(); 1441 if (value == -1) { 1442 in.close(); 1443 break; 1444 } 1445 result.append((char) value); 1446 } 1447 return result.toString(); 1448 } 1449 1450 private String readAscii(URLConnection connection) throws IOException { 1451 return readAscii(connection, Integer.MAX_VALUE); 1452 } 1453 1454 private void reliableSkip(InputStream in, int length) throws IOException { 1455 while (length > 0) { 1456 length -= in.skip(length); 1457 } 1458 } 1459 1460 private void assertGatewayTimeout(HttpURLConnection connection) throws IOException { 1461 try { 1462 connection.getInputStream(); 1463 fail(); 1464 } catch (FileNotFoundException expected) { 1465 } 1466 assertEquals(504, connection.getResponseCode()); 1467 assertEquals(-1, connection.getErrorStream().read()); 1468 assertEquals(ResponseSource.NONE + " 504", 1469 connection.getHeaderField(OkHeaders.RESPONSE_SOURCE)); 1470 } 1471 1472 enum TransferKind { 1473 CHUNKED() { 1474 @Override void setBody(MockResponse response, byte[] content, int chunkSize) 1475 throws IOException { 1476 response.setChunkedBody(content, chunkSize); 1477 } 1478 }, 1479 FIXED_LENGTH() { 1480 @Override void setBody(MockResponse response, byte[] content, int chunkSize) { 1481 response.setBody(content); 1482 } 1483 }, 1484 END_OF_STREAM() { 1485 @Override void setBody(MockResponse response, byte[] content, int chunkSize) { 1486 response.setBody(content); 1487 response.setSocketPolicy(DISCONNECT_AT_END); 1488 for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) { 1489 if (h.next().startsWith("Content-Length:")) { 1490 h.remove(); 1491 break; 1492 } 1493 } 1494 } 1495 }; 1496 1497 abstract void setBody(MockResponse response, byte[] content, int chunkSize) throws IOException; 1498 1499 void setBody(MockResponse response, String content, int chunkSize) throws IOException { 1500 setBody(response, content.getBytes("UTF-8"), chunkSize); 1501 } 1502 } 1503 1504 private <T> List<T> toListOrNull(T[] arrayOrNull) { 1505 return arrayOrNull != null ? Arrays.asList(arrayOrNull) : null; 1506 } 1507 1508 /** Returns a gzipped copy of {@code bytes}. */ 1509 public byte[] gzip(byte[] bytes) throws IOException { 1510 ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); 1511 OutputStream gzippedOut = new GZIPOutputStream(bytesOut); 1512 gzippedOut.write(bytes); 1513 gzippedOut.close(); 1514 return bytesOut.toByteArray(); 1515 } 1516 1517 private static class InsecureResponseCache extends ResponseCache { 1518 1519 private final ResponseCache delegate; 1520 1521 private InsecureResponseCache(ResponseCache delegate) { 1522 this.delegate = delegate; 1523 } 1524 1525 @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { 1526 return delegate.put(uri, connection); 1527 } 1528 1529 @Override public CacheResponse get(URI uri, String requestMethod, 1530 Map<String, List<String>> requestHeaders) throws IOException { 1531 final CacheResponse response = delegate.get(uri, requestMethod, requestHeaders); 1532 if (response instanceof SecureCacheResponse) { 1533 return new CacheResponse() { 1534 @Override public InputStream getBody() throws IOException { 1535 return response.getBody(); 1536 } 1537 @Override public Map<String, List<String>> getHeaders() throws IOException { 1538 return response.getHeaders(); 1539 } 1540 }; 1541 } 1542 return response; 1543 } 1544 } 1545 1546 /** 1547 * A trivial and non-thread-safe implementation of ResponseCache that uses an in-memory map to 1548 * cache GETs. 1549 */ 1550 private static class InMemoryResponseCache extends ResponseCache { 1551 1552 /** A request / response header that acts a bit like Vary but without the complexity. */ 1553 public static final String CACHE_VARIANT_HEADER = "CacheVariant"; 1554 1555 private static class Key { 1556 private final URI uri; 1557 private final String cacheVariant; 1558 1559 private Key(URI uri, String cacheVariant) { 1560 this.uri = uri; 1561 this.cacheVariant = cacheVariant; 1562 } 1563 1564 @Override 1565 public boolean equals(Object o) { 1566 if (this == o) { 1567 return true; 1568 } 1569 if (o == null || getClass() != o.getClass()) { 1570 return false; 1571 } 1572 1573 Key key = (Key) o; 1574 1575 if (cacheVariant != null ? !cacheVariant.equals(key.cacheVariant) 1576 : key.cacheVariant != null) { 1577 return false; 1578 } 1579 if (!uri.equals(key.uri)) { 1580 return false; 1581 } 1582 1583 return true; 1584 } 1585 1586 @Override 1587 public int hashCode() { 1588 int result = uri.hashCode(); 1589 result = 31 * result + (cacheVariant != null ? cacheVariant.hashCode() : 0); 1590 return result; 1591 } 1592 } 1593 1594 private class Entry { 1595 1596 private final URI uri; 1597 private final String cacheVariant; 1598 private final String method; 1599 private final Map<String, List<String>> responseHeaders; 1600 private final String cipherSuite; 1601 private final Certificate[] serverCertificates; 1602 private final Certificate[] localCertificates; 1603 private byte[] body; 1604 1605 public Entry(URI uri, URLConnection urlConnection) { 1606 this.uri = uri; 1607 HttpURLConnection httpUrlConnection = (HttpURLConnection) urlConnection; 1608 method = httpUrlConnection.getRequestMethod(); 1609 cacheVariant = urlConnection.getHeaderField(CACHE_VARIANT_HEADER); 1610 responseHeaders = urlConnection.getHeaderFields(); 1611 if (urlConnection instanceof HttpsURLConnection) { 1612 HttpsURLConnection httpsURLConnection = (HttpsURLConnection) urlConnection; 1613 cipherSuite = httpsURLConnection.getCipherSuite(); 1614 Certificate[] serverCertificates; 1615 try { 1616 serverCertificates = httpsURLConnection.getServerCertificates(); 1617 } catch (SSLPeerUnverifiedException e) { 1618 serverCertificates = null; 1619 } 1620 this.serverCertificates = serverCertificates; 1621 localCertificates = httpsURLConnection.getLocalCertificates(); 1622 } else { 1623 cipherSuite = null; 1624 serverCertificates = null; 1625 localCertificates = null; 1626 } 1627 } 1628 1629 public CacheResponse asCacheResponse() { 1630 if (!method.equals(this.method)) { 1631 return null; 1632 } 1633 1634 // Handle SSL 1635 if (cipherSuite != null) { 1636 return new SecureCacheResponse() { 1637 @Override 1638 public Map<String, List<String>> getHeaders() throws IOException { 1639 return responseHeaders; 1640 } 1641 1642 @Override 1643 public InputStream getBody() throws IOException { 1644 return new ByteArrayInputStream(body); 1645 } 1646 1647 @Override 1648 public String getCipherSuite() { 1649 return cipherSuite; 1650 } 1651 1652 @Override 1653 public List<Certificate> getLocalCertificateChain() { 1654 return localCertificates == null ? null : Arrays.asList(localCertificates); 1655 } 1656 1657 @Override 1658 public List<Certificate> getServerCertificateChain() throws SSLPeerUnverifiedException { 1659 if (serverCertificates == null) { 1660 throw new SSLPeerUnverifiedException("Test implementation"); 1661 } 1662 return Arrays.asList(serverCertificates); 1663 } 1664 1665 @Override 1666 public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { 1667 throw new UnsupportedOperationException(); 1668 } 1669 1670 @Override 1671 public Principal getLocalPrincipal() { 1672 throw new UnsupportedOperationException(); 1673 } 1674 }; 1675 } else { 1676 return new CacheResponse() { 1677 @Override 1678 public Map<String, List<String>> getHeaders() throws IOException { 1679 return responseHeaders; 1680 } 1681 1682 @Override 1683 public InputStream getBody() throws IOException { 1684 return new ByteArrayInputStream(body); 1685 } 1686 }; 1687 } 1688 } 1689 1690 public CacheRequest asCacheRequest() { 1691 return new CacheRequest() { 1692 @Override 1693 public OutputStream getBody() throws IOException { 1694 return new ByteArrayOutputStream() { 1695 @Override 1696 public void close() throws IOException { 1697 super.close(); 1698 body = toByteArray(); 1699 cache.put(Entry.this.key(), Entry.this); 1700 } 1701 }; 1702 } 1703 1704 @Override 1705 public void abort() { 1706 // No-op: close() puts the item in the cache, abort need not do anything. 1707 } 1708 }; 1709 } 1710 1711 private Key key() { 1712 return new Key(uri, cacheVariant); 1713 } 1714 } 1715 1716 private Map<Key, Entry> cache = new HashMap<Key, Entry>(); 1717 1718 @Override 1719 public CacheResponse get(URI uri, String method, Map<String, List<String>> requestHeaders) 1720 throws IOException { 1721 1722 if (!"GET".equals(method)) { 1723 return null; 1724 } 1725 1726 String cacheVariant = 1727 requestHeaders.containsKey(CACHE_VARIANT_HEADER) 1728 ? requestHeaders.get(CACHE_VARIANT_HEADER).get(0) : null; 1729 Key key = new Key(uri, cacheVariant); 1730 Entry entry = cache.get(key); 1731 if (entry == null) { 1732 return null; 1733 } 1734 return entry.asCacheResponse(); 1735 } 1736 1737 @Override 1738 public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException { 1739 if (!"GET".equals(((HttpURLConnection) urlConnection).getRequestMethod())) { 1740 return null; 1741 } 1742 1743 Entry entry = new Entry(uri, urlConnection); 1744 return entry.asCacheRequest(); 1745 } 1746 } 1747} 1748