URLConnectionTest.java revision 06e15e6c528fcb773bedb43e34b0577312570927
1/*
2 * Copyright (C) 2009 The Android Open Source Project
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 libcore.java.net;
18
19import java.io.BufferedReader;
20import java.io.ByteArrayOutputStream;
21import java.io.IOException;
22import java.io.InputStream;
23import java.io.InputStreamReader;
24import java.io.OutputStream;
25import java.net.Authenticator;
26import java.net.CacheRequest;
27import java.net.CacheResponse;
28import java.net.HttpRetryException;
29import java.net.HttpURLConnection;
30import java.net.InetAddress;
31import java.net.PasswordAuthentication;
32import java.net.ResponseCache;
33import java.net.SecureCacheResponse;
34import java.net.SocketTimeoutException;
35import java.net.URI;
36import java.net.URISyntaxException;
37import java.net.URL;
38import java.net.URLConnection;
39import java.security.Principal;
40import java.security.cert.Certificate;
41import java.security.cert.CertificateException;
42import java.security.cert.X509Certificate;
43import java.util.ArrayList;
44import java.util.Arrays;
45import java.util.Collections;
46import java.util.HashSet;
47import java.util.Iterator;
48import java.util.List;
49import java.util.Map;
50import java.util.Set;
51import java.util.concurrent.atomic.AtomicReference;
52import java.util.zip.GZIPInputStream;
53import java.util.zip.GZIPOutputStream;
54import javax.net.ssl.HostnameVerifier;
55import javax.net.ssl.HttpsURLConnection;
56import javax.net.ssl.SSLContext;
57import javax.net.ssl.SSLException;
58import javax.net.ssl.SSLSession;
59import javax.net.ssl.SSLSocketFactory;
60import javax.net.ssl.TrustManager;
61import javax.net.ssl.X509TrustManager;
62import libcore.javax.net.ssl.TestSSLContext;
63import tests.http.DefaultResponseCache;
64import tests.http.MockResponse;
65import tests.http.MockWebServer;
66import tests.http.RecordedRequest;
67
68public class URLConnectionTest extends junit.framework.TestCase {
69
70    private static final Authenticator SIMPLE_AUTHENTICATOR = new Authenticator() {
71        protected PasswordAuthentication getPasswordAuthentication() {
72            return new PasswordAuthentication("username", "password".toCharArray());
73        }
74    };
75
76    private MockWebServer server = new MockWebServer();
77    private String hostname;
78
79    @Override protected void setUp() throws Exception {
80        super.setUp();
81        hostname = InetAddress.getLocalHost().getHostName();
82    }
83
84    @Override protected void tearDown() throws Exception {
85        ResponseCache.setDefault(null);
86        Authenticator.setDefault(null);
87        System.clearProperty("proxyHost");
88        System.clearProperty("proxyPort");
89        System.clearProperty("http.proxyHost");
90        System.clearProperty("http.proxyPort");
91        System.clearProperty("https.proxyHost");
92        System.clearProperty("https.proxyPort");
93        server.shutdown();
94        super.tearDown();
95    }
96
97    public void testRequestHeaders() throws IOException, InterruptedException {
98        server.enqueue(new MockResponse());
99        server.play();
100
101        HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
102        urlConnection.addRequestProperty("D", "e");
103        urlConnection.addRequestProperty("D", "f");
104        Map<String, List<String>> requestHeaders = urlConnection.getRequestProperties();
105        assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("D")));
106        try {
107            requestHeaders.put("G", Arrays.asList("h"));
108            fail("Modified an unmodifiable view.");
109        } catch (UnsupportedOperationException expected) {
110        }
111        try {
112            requestHeaders.get("D").add("i");
113            fail("Modified an unmodifiable view.");
114        } catch (UnsupportedOperationException expected) {
115        }
116        try {
117            urlConnection.setRequestProperty(null, "j");
118            fail();
119        } catch (NullPointerException expected) {
120        }
121        try {
122            urlConnection.addRequestProperty(null, "k");
123            fail();
124        } catch (NullPointerException expected) {
125        }
126        urlConnection.setRequestProperty("NullValue", null); // should fail silently!
127        urlConnection.addRequestProperty("AnotherNullValue", null);  // should fail silently!
128
129        urlConnection.getResponseCode();
130        RecordedRequest request = server.takeRequest();
131        assertContains(request.getHeaders(), "D: e");
132        assertContains(request.getHeaders(), "D: f");
133        assertContainsNoneMatching(request.getHeaders(), "NullValue.*");
134        assertContainsNoneMatching(request.getHeaders(), "AnotherNullValue.*");
135        assertContainsNoneMatching(request.getHeaders(), "G:.*");
136        assertContainsNoneMatching(request.getHeaders(), "null:.*");
137
138        try {
139            urlConnection.addRequestProperty("N", "o");
140            fail("Set header after connect");
141        } catch (IllegalStateException expected) {
142        }
143        try {
144            urlConnection.setRequestProperty("P", "q");
145            fail("Set header after connect");
146        } catch (IllegalStateException expected) {
147        }
148    }
149
150    public void testResponseHeaders() throws IOException, InterruptedException {
151        server.enqueue(new MockResponse()
152                .setStatus("HTTP/1.0 200 Fantastic")
153                .addHeader("A: b")
154                .addHeader("A: c")
155                .setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8));
156        server.play();
157
158        HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
159        assertEquals(200, urlConnection.getResponseCode());
160        assertEquals("Fantastic", urlConnection.getResponseMessage());
161        assertEquals("HTTP/1.0 200 Fantastic", urlConnection.getHeaderField(null));
162        Map<String, List<String>> responseHeaders = urlConnection.getHeaderFields();
163        assertEquals(Arrays.asList("HTTP/1.0 200 Fantastic"), responseHeaders.get(null));
164        assertEquals(newSet("b", "c"), new HashSet<String>(responseHeaders.get("A")));
165        try {
166            responseHeaders.put("N", Arrays.asList("o"));
167            fail("Modified an unmodifiable view.");
168        } catch (UnsupportedOperationException expected) {
169        }
170        try {
171            responseHeaders.get("A").add("d");
172            fail("Modified an unmodifiable view.");
173        } catch (UnsupportedOperationException expected) {
174        }
175    }
176
177    // Check that if we don't read to the end of a response, the next request on the
178    // recycled connection doesn't get the unread tail of the first request's response.
179    // http://code.google.com/p/android/issues/detail?id=2939
180    public void test_2939() throws Exception {
181        MockResponse response = new MockResponse().setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8);
182
183        server.enqueue(response);
184        server.enqueue(response);
185        server.play();
186
187        assertContent("ABCDE", server.getUrl("/").openConnection(), 5);
188        assertContent("ABCDE", server.getUrl("/").openConnection(), 5);
189    }
190
191    // Check that we recognize a few basic mime types by extension.
192    // http://code.google.com/p/android/issues/detail?id=10100
193    public void test_10100() throws Exception {
194        assertEquals("image/jpeg", URLConnection.guessContentTypeFromName("someFile.jpg"));
195        assertEquals("application/pdf", URLConnection.guessContentTypeFromName("stuff.pdf"));
196    }
197
198    public void testConnectionsArePooled() throws Exception {
199        MockResponse response = new MockResponse().setBody("ABCDEFGHIJKLMNOPQR");
200
201        server.enqueue(response);
202        server.enqueue(response);
203        server.enqueue(response);
204        server.play();
205
206        assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/foo").openConnection());
207        assertEquals(0, server.takeRequest().getSequenceNumber());
208        assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/bar?baz=quux").openConnection());
209        assertEquals(1, server.takeRequest().getSequenceNumber());
210        assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/z").openConnection());
211        assertEquals(2, server.takeRequest().getSequenceNumber());
212    }
213
214    public void testChunkedConnectionsArePooled() throws Exception {
215        MockResponse response = new MockResponse().setChunkedBody("ABCDEFGHIJKLMNOPQR", 5);
216
217        server.enqueue(response);
218        server.enqueue(response);
219        server.enqueue(response);
220        server.play();
221
222        assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/foo").openConnection());
223        assertEquals(0, server.takeRequest().getSequenceNumber());
224        assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/bar?baz=quux").openConnection());
225        assertEquals(1, server.takeRequest().getSequenceNumber());
226        assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/z").openConnection());
227        assertEquals(2, server.takeRequest().getSequenceNumber());
228    }
229
230    enum WriteKind { BYTE_BY_BYTE, SMALL_BUFFERS, LARGE_BUFFERS }
231
232    public void test_chunkedUpload_byteByByte() throws Exception {
233        doUpload(TransferKind.CHUNKED, WriteKind.BYTE_BY_BYTE);
234    }
235
236    public void test_chunkedUpload_smallBuffers() throws Exception {
237        doUpload(TransferKind.CHUNKED, WriteKind.SMALL_BUFFERS);
238    }
239
240    public void test_chunkedUpload_largeBuffers() throws Exception {
241        doUpload(TransferKind.CHUNKED, WriteKind.LARGE_BUFFERS);
242    }
243
244    public void test_fixedLengthUpload_byteByByte() throws Exception {
245        doUpload(TransferKind.FIXED_LENGTH, WriteKind.BYTE_BY_BYTE);
246    }
247
248    public void test_fixedLengthUpload_smallBuffers() throws Exception {
249        doUpload(TransferKind.FIXED_LENGTH, WriteKind.SMALL_BUFFERS);
250    }
251
252    public void test_fixedLengthUpload_largeBuffers() throws Exception {
253        doUpload(TransferKind.FIXED_LENGTH, WriteKind.LARGE_BUFFERS);
254    }
255
256    private void doUpload(TransferKind uploadKind, WriteKind writeKind) throws Exception {
257        int n = 512*1024;
258        server.setBodyLimit(0);
259        server.enqueue(new MockResponse());
260        server.play();
261
262        HttpURLConnection conn = (HttpURLConnection) server.getUrl("/").openConnection();
263        conn.setDoOutput(true);
264        conn.setRequestMethod("POST");
265        if (uploadKind == TransferKind.CHUNKED) {
266            conn.setChunkedStreamingMode(-1);
267        } else {
268            conn.setFixedLengthStreamingMode(n);
269        }
270        OutputStream out = conn.getOutputStream();
271        if (writeKind == WriteKind.BYTE_BY_BYTE) {
272            for (int i = 0; i < n; ++i) {
273                out.write('x');
274            }
275        } else {
276            byte[] buf = new byte[writeKind == WriteKind.SMALL_BUFFERS ? 256 : 64*1024];
277            Arrays.fill(buf, (byte) 'x');
278            for (int i = 0; i < n; i += buf.length) {
279                out.write(buf, 0, Math.min(buf.length, n - i));
280            }
281        }
282        out.close();
283        assertEquals(200, conn.getResponseCode());
284        RecordedRequest request = server.takeRequest();
285        assertEquals(n, request.getBodySize());
286        if (uploadKind == TransferKind.CHUNKED) {
287            assertTrue(request.getChunkSizes().size() > 0);
288        } else {
289            assertTrue(request.getChunkSizes().isEmpty());
290        }
291    }
292
293    /**
294     * Test that response caching is consistent with the RI and the spec.
295     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
296     */
297    public void test_responseCaching() throws Exception {
298        // Test each documented HTTP/1.1 code, plus the first unused value in each range.
299        // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
300
301        // We can't test 100 because it's not really a response.
302        // assertCached(false, 100);
303        assertCached(false, 101);
304        assertCached(false, 102);
305        assertCached(true,  200);
306        assertCached(false, 201);
307        assertCached(false, 202);
308        assertCached(true,  203);
309        assertCached(false, 204);
310        assertCached(false, 205);
311        assertCached(true,  206);
312        assertCached(false, 207);
313        // (See test_responseCaching_300.)
314        assertCached(true,  301);
315        for (int i = 302; i <= 308; ++i) {
316            assertCached(false, i);
317        }
318        for (int i = 400; i <= 406; ++i) {
319            assertCached(false, i);
320        }
321        // (See test_responseCaching_407.)
322        assertCached(false, 408);
323        assertCached(false, 409);
324        // (See test_responseCaching_410.)
325        for (int i = 411; i <= 418; ++i) {
326            assertCached(false, i);
327        }
328        for (int i = 500; i <= 506; ++i) {
329            assertCached(false, i);
330        }
331    }
332
333    public void test_responseCaching_300() throws Exception {
334        // TODO: fix this for android
335        assertCached(false, 300);
336    }
337
338    /**
339     * Response code 407 should only come from proxy servers. Android's client
340     * throws if it is sent by an origin server.
341     */
342    public void testOriginServerSends407() throws Exception {
343        server.enqueue(new MockResponse().setResponseCode(407));
344        server.play();
345
346        URL url = server.getUrl("/");
347        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
348        try {
349            conn.getResponseCode();
350            fail();
351        } catch (IOException expected) {
352        }
353    }
354
355    public void test_responseCaching_410() throws Exception {
356        // the HTTP spec permits caching 410s, but the RI doesn't.
357        assertCached(false, 410);
358    }
359
360    private void assertCached(boolean shouldPut, int responseCode) throws Exception {
361        server = new MockWebServer();
362        server.enqueue(new MockResponse()
363                .setResponseCode(responseCode)
364                .setBody("ABCDE")
365                .addHeader("WWW-Authenticate: challenge"));
366        server.play();
367
368        DefaultResponseCache responseCache = new DefaultResponseCache();
369        ResponseCache.setDefault(responseCache);
370        URL url = server.getUrl("/");
371        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
372        assertEquals(responseCode, conn.getResponseCode());
373
374        // exhaust the content stream
375        try {
376            // TODO: remove special case once testUnauthorizedResponseHandling() is fixed
377            if (responseCode != 401) {
378                readAscii(conn.getInputStream(), Integer.MAX_VALUE);
379            }
380        } catch (IOException ignored) {
381        }
382
383        Set<URI> expectedCachedUris = shouldPut
384                ? Collections.singleton(url.toURI())
385                : Collections.<URI>emptySet();
386        assertEquals(Integer.toString(responseCode),
387                expectedCachedUris, responseCache.getContents().keySet());
388        server.shutdown(); // tearDown() isn't sufficient; this test starts multiple servers
389    }
390
391    public void testConnectViaHttps() throws IOException, InterruptedException {
392        TestSSLContext testSSLContext = TestSSLContext.create();
393
394        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
395        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
396        server.play();
397
398        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/foo").openConnection();
399        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
400
401        assertContent("this response comes via HTTPS", connection);
402
403        RecordedRequest request = server.takeRequest();
404        assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
405    }
406
407    public void testConnectViaHttpsReusingConnections() throws IOException, InterruptedException {
408        TestSSLContext testSSLContext = TestSSLContext.create();
409
410        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
411        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
412        server.enqueue(new MockResponse().setBody("another response via HTTPS"));
413        server.play();
414
415        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
416        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
417        assertContent("this response comes via HTTPS", connection);
418
419        connection = (HttpsURLConnection) server.getUrl("/").openConnection();
420        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
421        assertContent("another response via HTTPS", connection);
422
423        assertEquals(0, server.takeRequest().getSequenceNumber());
424        assertEquals(1, server.takeRequest().getSequenceNumber());
425    }
426
427    public void testConnectViaHttpsReusingConnectionsDifferentFactories()
428            throws IOException, InterruptedException {
429        TestSSLContext testSSLContext = TestSSLContext.create();
430
431        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
432        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
433        server.enqueue(new MockResponse().setBody("another response via HTTPS"));
434        server.play();
435
436        // install a custom SSL socket factory so the server can be authorized
437        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
438        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
439        assertContent("this response comes via HTTPS", connection);
440
441        connection = (HttpsURLConnection) server.getUrl("/").openConnection();
442        try {
443            readAscii(connection.getInputStream(), Integer.MAX_VALUE);
444            fail("without an SSL socket factory, the connection should fail");
445        } catch (SSLException expected) {
446        }
447    }
448
449    public void testConnectViaHttpsWithSSLFallback() throws IOException, InterruptedException {
450        TestSSLContext testSSLContext = TestSSLContext.create();
451
452        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
453        server.enqueue(new MockResponse().setDisconnectAtStart(true));
454        server.enqueue(new MockResponse().setBody("this response comes via SSL"));
455        server.play();
456
457        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/foo").openConnection();
458        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
459
460        assertContent("this response comes via SSL", connection);
461
462        RecordedRequest request = server.takeRequest();
463        assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
464    }
465
466    public void testConnectViaProxyUsingProxyArg() throws Exception {
467        testConnectViaProxy(ProxyConfig.CREATE_ARG);
468    }
469
470    public void testConnectViaProxyUsingProxySystemProperty() throws Exception {
471        testConnectViaProxy(ProxyConfig.PROXY_SYSTEM_PROPERTY);
472    }
473
474    public void testConnectViaProxyUsingHttpProxySystemProperty() throws Exception {
475        testConnectViaProxy(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
476    }
477
478    private void testConnectViaProxy(ProxyConfig proxyConfig) throws Exception {
479        MockResponse mockResponse = new MockResponse().setBody("this response comes via a proxy");
480        server.enqueue(mockResponse);
481        server.play();
482
483        URL url = new URL("http://android.com/foo");
484        HttpURLConnection connection = proxyConfig.connect(server, url);
485        assertContent("this response comes via a proxy", connection);
486
487        RecordedRequest request = server.takeRequest();
488        assertEquals("GET http://android.com/foo HTTP/1.1", request.getRequestLine());
489        assertContains(request.getHeaders(), "Host: android.com");
490    }
491
492    public void testContentDisagreesWithContentLengthHeader() throws IOException {
493        server.enqueue(new MockResponse()
494                .setBody("abc\r\nYOU SHOULD NOT SEE THIS")
495                .clearHeaders()
496                .addHeader("Content-Length: 3"));
497        server.play();
498
499        assertContent("abc", server.getUrl("/").openConnection());
500    }
501
502    public void testContentDisagreesWithChunkedHeader() throws IOException {
503        MockResponse mockResponse = new MockResponse();
504        mockResponse.setChunkedBody("abc", 3);
505        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
506        bytesOut.write(mockResponse.getBody());
507        bytesOut.write("\r\nYOU SHOULD NOT SEE THIS".getBytes());
508        mockResponse.setBody(bytesOut.toByteArray());
509        mockResponse.clearHeaders();
510        mockResponse.addHeader("Transfer-encoding: chunked");
511
512        server.enqueue(mockResponse);
513        server.play();
514
515        assertContent("abc", server.getUrl("/").openConnection());
516    }
517
518    public void testConnectViaHttpProxyToHttpsUsingProxyArg() throws Exception {
519        testConnectViaHttpProxyToHttps(ProxyConfig.CREATE_ARG);
520    }
521
522    /**
523     * We weren't honoring all of the appropriate proxy system properties when
524     * connecting via HTTPS. http://b/3097518
525     */
526    public void testConnectViaHttpProxyToHttpsUsingProxySystemProperty() throws Exception {
527        testConnectViaHttpProxyToHttps(ProxyConfig.PROXY_SYSTEM_PROPERTY);
528    }
529
530    public void testConnectViaHttpProxyToHttpsUsingHttpProxySystemProperty() throws Exception {
531        testConnectViaHttpProxyToHttps(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
532    }
533
534    public void testConnectViaHttpProxyToHttpsUsingHttpsProxySystemProperty() throws Exception {
535        testConnectViaHttpProxyToHttps(ProxyConfig.HTTPS_PROXY_SYSTEM_PROPERTY);
536    }
537
538    /**
539     * We were verifying the wrong hostname when connecting to an HTTPS site
540     * through a proxy. http://b/3097277
541     */
542    private void testConnectViaHttpProxyToHttps(ProxyConfig proxyConfig) throws Exception {
543        TestSSLContext testSSLContext = TestSSLContext.create();
544        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
545
546        server.useHttps(testSSLContext.serverContext.getSocketFactory(), true);
547        server.enqueue(new MockResponse().clearHeaders()); // for CONNECT
548        server.enqueue(new MockResponse().setBody("this response comes via a secure proxy"));
549        server.play();
550
551        URL url = new URL("https://android.com/foo");
552        HttpsURLConnection connection = (HttpsURLConnection) proxyConfig.connect(server, url);
553        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
554        connection.setHostnameVerifier(hostnameVerifier);
555
556        assertContent("this response comes via a secure proxy", connection);
557
558        RecordedRequest connect = server.takeRequest();
559        assertEquals("Connect line failure on proxy",
560                "CONNECT android.com:443 HTTP/1.1", connect.getRequestLine());
561        assertContains(connect.getHeaders(), "Host: android.com");
562
563        RecordedRequest get = server.takeRequest();
564        assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
565        assertContains(get.getHeaders(), "Host: android.com");
566        assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
567    }
568
569    /**
570     * Test which headers are sent unencrypted to the HTTP proxy.
571     */
572    public void testProxyConnectIncludesProxyHeadersOnly()
573            throws IOException, InterruptedException {
574        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
575        TestSSLContext testSSLContext = TestSSLContext.create();
576
577        server.useHttps(testSSLContext.serverContext.getSocketFactory(), true);
578        server.enqueue(new MockResponse().clearHeaders()); // for CONNECT
579        server.enqueue(new MockResponse().setBody("encrypted response from the origin server"));
580        server.play();
581
582        URL url = new URL("https://android.com/foo");
583        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(
584                server.toProxyAddress());
585        connection.addRequestProperty("Private", "Secret");
586        connection.addRequestProperty("Proxy-Authorization", "bar");
587        connection.addRequestProperty("User-Agent", "baz");
588        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
589        connection.setHostnameVerifier(hostnameVerifier);
590        assertContent("encrypted response from the origin server", connection);
591
592        RecordedRequest connect = server.takeRequest();
593        assertContainsNoneMatching(connect.getHeaders(), "Private.*");
594        assertContains(connect.getHeaders(), "Proxy-Authorization: bar");
595        assertContains(connect.getHeaders(), "User-Agent: baz");
596        assertContains(connect.getHeaders(), "Host: android.com");
597        assertContains(connect.getHeaders(), "Proxy-Connection: Keep-Alive");
598
599        RecordedRequest get = server.takeRequest();
600        assertContains(get.getHeaders(), "Private: Secret");
601        assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
602    }
603
604    public void testDisconnectedConnection() throws IOException {
605        server.enqueue(new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"));
606        server.play();
607
608        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
609        InputStream in = connection.getInputStream();
610        assertEquals('A', (char) in.read());
611        connection.disconnect();
612        try {
613            in.read();
614            fail("Expected a connection closed exception");
615        } catch (IOException expected) {
616        }
617    }
618
619    public void testResponseCachingAndInputStreamSkipWithFixedLength() throws IOException {
620        testResponseCaching(TransferKind.FIXED_LENGTH);
621    }
622
623    public void testResponseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException {
624        testResponseCaching(TransferKind.CHUNKED);
625    }
626
627    public void testResponseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException {
628        testResponseCaching(TransferKind.END_OF_STREAM);
629    }
630
631    /**
632     * HttpURLConnection.getInputStream().skip(long) causes ResponseCache corruption
633     * http://code.google.com/p/android/issues/detail?id=8175
634     */
635    private void testResponseCaching(TransferKind transferKind) throws IOException {
636        MockResponse response = new MockResponse()
637                .setStatus("HTTP/1.1 200 Fantastic");
638        transferKind.setBody(response, "I love puppies but hate spiders", 1);
639        server.enqueue(response);
640        server.play();
641
642        DefaultResponseCache cache = new DefaultResponseCache();
643        ResponseCache.setDefault(cache);
644
645        // Make sure that calling skip() doesn't omit bytes from the cache.
646        HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
647        InputStream in = urlConnection.getInputStream();
648        assertEquals("I love ", readAscii(in, "I love ".length()));
649        reliableSkip(in, "puppies but hate ".length());
650        assertEquals("spiders", readAscii(in, "spiders".length()));
651        assertEquals(-1, in.read());
652        in.close();
653        assertEquals(1, cache.getSuccessCount());
654        assertEquals(0, cache.getAbortCount());
655
656        urlConnection = (HttpURLConnection) server.getUrl("/").openConnection(); // cached!
657        in = urlConnection.getInputStream();
658        assertEquals("I love puppies but hate spiders",
659                readAscii(in, "I love puppies but hate spiders".length()));
660        assertEquals(200, urlConnection.getResponseCode());
661        assertEquals("Fantastic", urlConnection.getResponseMessage());
662
663        assertEquals(-1, in.read());
664        assertEquals(1, cache.getMissCount());
665        assertEquals(1, cache.getHitCount());
666        assertEquals(1, cache.getSuccessCount());
667        assertEquals(0, cache.getAbortCount());
668    }
669
670    public void testSecureResponseCaching() throws IOException {
671        TestSSLContext testSSLContext = TestSSLContext.create();
672        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
673        server.enqueue(new MockResponse().setBody("ABC"));
674        server.play();
675
676        DefaultResponseCache cache = new DefaultResponseCache();
677        ResponseCache.setDefault(cache);
678
679        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
680        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
681        assertEquals("ABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
682
683        // OpenJDK 6 fails on this line, complaining that the connection isn't open yet
684        String suite = connection.getCipherSuite();
685        List<Certificate> localCerts = toListOrNull(connection.getLocalCertificates());
686        List<Certificate> serverCerts = toListOrNull(connection.getServerCertificates());
687        Principal peerPrincipal = connection.getPeerPrincipal();
688        Principal localPrincipal = connection.getLocalPrincipal();
689
690        connection = (HttpsURLConnection) server.getUrl("/").openConnection(); // cached!
691        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
692        assertEquals("ABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
693
694        assertEquals(1, cache.getMissCount());
695        assertEquals(1, cache.getHitCount());
696
697        assertEquals(suite, connection.getCipherSuite());
698        assertEquals(localCerts, toListOrNull(connection.getLocalCertificates()));
699        assertEquals(serverCerts, toListOrNull(connection.getServerCertificates()));
700        assertEquals(peerPrincipal, connection.getPeerPrincipal());
701        assertEquals(localPrincipal, connection.getLocalPrincipal());
702    }
703
704    public void testCacheReturnsInsecureResponseForSecureRequest() throws IOException {
705        TestSSLContext testSSLContext = TestSSLContext.create();
706        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
707        server.enqueue(new MockResponse().setBody("ABC"));
708        server.enqueue(new MockResponse().setBody("DEF"));
709        server.play();
710
711        ResponseCache insecureResponseCache = new InsecureResponseCache();
712        ResponseCache.setDefault(insecureResponseCache);
713
714        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
715        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
716        assertEquals("ABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
717
718        connection = (HttpsURLConnection) server.getUrl("/").openConnection(); // not cached!
719        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
720        assertEquals("DEF", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
721    }
722
723    public void testResponseCachingAndRedirects() throws IOException {
724        server.enqueue(new MockResponse()
725                .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
726                .addHeader("Location: /foo"));
727        server.enqueue(new MockResponse().setBody("ABC"));
728        server.enqueue(new MockResponse().setBody("DEF"));
729        server.play();
730
731        DefaultResponseCache cache = new DefaultResponseCache();
732        ResponseCache.setDefault(cache);
733
734        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
735        assertEquals("ABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
736
737        connection = (HttpURLConnection) server.getUrl("/").openConnection(); // cached!
738        assertEquals("ABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
739
740        assertEquals(2, cache.getMissCount()); // 1 redirect + 1 final response = 2
741        assertEquals(2, cache.getHitCount());
742    }
743
744    public void testSecureResponseCachingAndRedirects() throws IOException {
745        TestSSLContext testSSLContext = TestSSLContext.create();
746        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
747        server.enqueue(new MockResponse()
748                .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
749                .addHeader("Location: /foo"));
750        server.enqueue(new MockResponse().setBody("ABC"));
751        server.enqueue(new MockResponse().setBody("DEF"));
752        server.play();
753
754        DefaultResponseCache cache = new DefaultResponseCache();
755        ResponseCache.setDefault(cache);
756
757        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
758        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
759        assertEquals("ABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
760
761        connection = (HttpsURLConnection) server.getUrl("/").openConnection(); // cached!
762        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
763        assertEquals("ABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
764
765        assertEquals(2, cache.getMissCount()); // 1 redirect + 1 final response = 2
766        assertEquals(2, cache.getHitCount());
767    }
768
769    public void testResponseCacheRequestHeaders() throws IOException, URISyntaxException {
770        server.enqueue(new MockResponse().setBody("ABC"));
771        server.play();
772
773        final AtomicReference<Map<String, List<String>>> requestHeadersRef
774                = new AtomicReference<Map<String, List<String>>>();
775        ResponseCache.setDefault(new ResponseCache() {
776            @Override public CacheResponse get(URI uri, String requestMethod,
777                    Map<String, List<String>> requestHeaders) throws IOException {
778                requestHeadersRef.set(requestHeaders);
779                return null;
780            }
781            @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException {
782                return null;
783            }
784        });
785
786        URL url = server.getUrl("/");
787        URLConnection urlConnection = url.openConnection();
788        urlConnection.addRequestProperty("A", "android");
789        readAscii(urlConnection.getInputStream(), Integer.MAX_VALUE);
790        assertEquals(Arrays.asList("android"), requestHeadersRef.get().get("A"));
791    }
792
793    private void reliableSkip(InputStream in, int length) throws IOException {
794        while (length > 0) {
795            length -= in.skip(length);
796        }
797    }
798
799    /**
800     * Reads {@code count} characters from the stream. If the stream is
801     * exhausted before {@code count} characters can be read, the remaining
802     * characters are returned and the stream is closed.
803     */
804    private String readAscii(InputStream in, int count) throws IOException {
805        StringBuilder result = new StringBuilder();
806        for (int i = 0; i < count; i++) {
807            int value = in.read();
808            if (value == -1) {
809                in.close();
810                break;
811            }
812            result.append((char) value);
813        }
814        return result.toString();
815    }
816
817    public void testServerDisconnectsPrematurelyWithContentLengthHeader() throws IOException {
818        testServerPrematureDisconnect(TransferKind.FIXED_LENGTH);
819    }
820
821    public void testServerDisconnectsPrematurelyWithChunkedEncoding() throws IOException {
822        testServerPrematureDisconnect(TransferKind.CHUNKED);
823    }
824
825    public void testServerDisconnectsPrematurelyWithNoLengthHeaders() throws IOException {
826        /*
827         * Intentionally empty. This case doesn't make sense because there's no
828         * such thing as a premature disconnect when the disconnect itself
829         * indicates the end of the data stream.
830         */
831    }
832
833    private void testServerPrematureDisconnect(TransferKind transferKind) throws IOException {
834        MockResponse response = new MockResponse();
835        transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16);
836        server.enqueue(truncateViolently(response, 16));
837        server.enqueue(new MockResponse().setBody("Request #2"));
838        server.play();
839
840        DefaultResponseCache cache = new DefaultResponseCache();
841        ResponseCache.setDefault(cache);
842
843        BufferedReader reader = new BufferedReader(new InputStreamReader(
844                server.getUrl("/").openConnection().getInputStream()));
845        assertEquals("ABCDE", reader.readLine());
846        try {
847            reader.readLine();
848            fail("This implementation silently ignored a truncated HTTP body.");
849        } catch (IOException expected) {
850        }
851
852        assertEquals(1, cache.getAbortCount());
853        assertEquals(0, cache.getSuccessCount());
854        assertContent("Request #2", server.getUrl("/").openConnection());
855        assertEquals(1, cache.getAbortCount());
856        assertEquals(1, cache.getSuccessCount());
857    }
858
859    public void testClientPrematureDisconnectWithContentLengthHeader() throws IOException {
860        testClientPrematureDisconnect(TransferKind.FIXED_LENGTH);
861    }
862
863    public void testClientPrematureDisconnectWithChunkedEncoding() throws IOException {
864        testClientPrematureDisconnect(TransferKind.CHUNKED);
865    }
866
867    public void testClientPrematureDisconnectWithNoLengthHeaders() throws IOException {
868        testClientPrematureDisconnect(TransferKind.END_OF_STREAM);
869    }
870
871    private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException {
872        MockResponse response = new MockResponse();
873        transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024);
874        server.enqueue(response);
875        server.enqueue(new MockResponse().setBody("Request #2"));
876        server.play();
877
878        DefaultResponseCache cache = new DefaultResponseCache();
879        ResponseCache.setDefault(cache);
880
881        InputStream in = server.getUrl("/").openConnection().getInputStream();
882        assertEquals("ABCDE", readAscii(in, 5));
883        in.close();
884        try {
885            in.read();
886            fail("Expected an IOException because the stream is closed.");
887        } catch (IOException expected) {
888        }
889
890        assertEquals(1, cache.getAbortCount());
891        assertEquals(0, cache.getSuccessCount());
892        assertContent("Request #2", server.getUrl("/").openConnection());
893        assertEquals(1, cache.getAbortCount());
894        assertEquals(1, cache.getSuccessCount());
895    }
896
897    /**
898     * Shortens the body of {@code response} but not the corresponding headers.
899     * Only useful to test how clients respond to the premature conclusion of
900     * the HTTP body.
901     */
902    private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) {
903        response.setDisconnectAtEnd(true);
904        List<String> headers = new ArrayList<String>(response.getHeaders());
905        response.setBody(Arrays.copyOfRange(response.getBody(), 0, numBytesToKeep));
906        response.getHeaders().clear();
907        response.getHeaders().addAll(headers);
908        return response;
909    }
910
911    public void testMarkAndResetWithContentLengthHeader() throws IOException {
912        testMarkAndReset(TransferKind.FIXED_LENGTH);
913    }
914
915    public void testMarkAndResetWithChunkedEncoding() throws IOException {
916        testMarkAndReset(TransferKind.CHUNKED);
917    }
918
919    public void testMarkAndResetWithNoLengthHeaders() throws IOException {
920        testMarkAndReset(TransferKind.END_OF_STREAM);
921    }
922
923    public void testMarkAndReset(TransferKind transferKind) throws IOException {
924        MockResponse response = new MockResponse();
925        transferKind.setBody(response, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1024);
926        server.enqueue(response);
927        server.play();
928
929        DefaultResponseCache cache = new DefaultResponseCache();
930        ResponseCache.setDefault(cache);
931
932        InputStream in = server.getUrl("/").openConnection().getInputStream();
933        assertFalse("This implementation claims to support mark().", in.markSupported());
934        in.mark(5);
935        assertEquals("ABCDE", readAscii(in, 5));
936        try {
937            in.reset();
938            fail();
939        } catch (IOException expected) {
940        }
941        assertEquals("FGHIJKLMNOPQRSTUVWXYZ", readAscii(in, Integer.MAX_VALUE));
942
943        assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", server.getUrl("/").openConnection());
944        assertEquals(1, cache.getSuccessCount());
945        assertEquals(1, cache.getHitCount());
946    }
947
948    /**
949     * We've had a bug where we forget the HTTP response when we see response
950     * code 401. This causes a new HTTP request to be issued for every call into
951     * the URLConnection.
952     */
953    public void testUnauthorizedResponseHandling() throws IOException {
954        MockResponse response = new MockResponse()
955                .addHeader("WWW-Authenticate: challenge")
956                .setResponseCode(401) // UNAUTHORIZED
957                .setBody("Unauthorized");
958        server.enqueue(response);
959        server.enqueue(response);
960        server.enqueue(response);
961        server.play();
962
963        URL url = server.getUrl("/");
964        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
965
966        assertEquals(401, conn.getResponseCode());
967        assertEquals(401, conn.getResponseCode());
968        assertEquals(401, conn.getResponseCode());
969        assertEquals(1, server.getRequestCount());
970    }
971
972    public void testNonHexChunkSize() throws IOException {
973        server.enqueue(new MockResponse()
974                .setBody("5\r\nABCDE\r\nG\r\nFGHIJKLMNOPQRSTU\r\n0\r\n\r\n")
975                .clearHeaders()
976                .addHeader("Transfer-encoding: chunked"));
977        server.play();
978
979        URLConnection connection = server.getUrl("/").openConnection();
980        try {
981            readAscii(connection.getInputStream(), Integer.MAX_VALUE);
982            fail();
983        } catch (IOException e) {
984        }
985    }
986
987    public void testMissingChunkBody() throws IOException {
988        server.enqueue(new MockResponse()
989                .setBody("5")
990                .clearHeaders()
991                .addHeader("Transfer-encoding: chunked")
992                .setDisconnectAtEnd(true));
993        server.play();
994
995        URLConnection connection = server.getUrl("/").openConnection();
996        try {
997            readAscii(connection.getInputStream(), Integer.MAX_VALUE);
998            fail();
999        } catch (IOException e) {
1000        }
1001    }
1002
1003    /**
1004     * This test checks whether connections are gzipped by default. This
1005     * behavior in not required by the API, so a failure of this test does not
1006     * imply a bug in the implementation.
1007     */
1008    public void testGzipEncodingEnabledByDefault() throws IOException, InterruptedException {
1009        server.enqueue(new MockResponse()
1010                .setBody(gzip("ABCABCABC".getBytes("UTF-8")))
1011                .addHeader("Content-Encoding: gzip"));
1012        server.play();
1013
1014        URLConnection connection = server.getUrl("/").openConnection();
1015        assertEquals("ABCABCABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1016        assertNull(connection.getContentEncoding());
1017
1018        RecordedRequest request = server.takeRequest();
1019        assertContains(request.getHeaders(), "Accept-Encoding: gzip");
1020    }
1021
1022    public void testClientConfiguredGzipContentEncoding() throws Exception {
1023        server.enqueue(new MockResponse()
1024                .setBody(gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8")))
1025                .addHeader("Content-Encoding: gzip"));
1026        server.play();
1027
1028        URLConnection connection = server.getUrl("/").openConnection();
1029        connection.addRequestProperty("Accept-Encoding", "gzip");
1030        InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
1031        assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", readAscii(gunzippedIn, Integer.MAX_VALUE));
1032
1033        RecordedRequest request = server.takeRequest();
1034        assertContains(request.getHeaders(), "Accept-Encoding: gzip");
1035    }
1036
1037    public void testGzipAndConnectionReuseWithFixedLength() throws Exception {
1038        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH);
1039    }
1040
1041    public void testGzipAndConnectionReuseWithChunkedEncoding() throws Exception {
1042        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED);
1043    }
1044
1045    public void testClientConfiguredCustomContentEncoding() throws Exception {
1046        server.enqueue(new MockResponse()
1047                .setBody("ABCDE")
1048                .addHeader("Content-Encoding: custom"));
1049        server.play();
1050
1051        URLConnection connection = server.getUrl("/").openConnection();
1052        connection.addRequestProperty("Accept-Encoding", "custom");
1053        assertEquals("ABCDE", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1054
1055        RecordedRequest request = server.takeRequest();
1056        assertContains(request.getHeaders(), "Accept-Encoding: custom");
1057    }
1058
1059    /**
1060     * Test a bug where gzip input streams weren't exhausting the input stream,
1061     * which corrupted the request that followed.
1062     * http://code.google.com/p/android/issues/detail?id=7059
1063     */
1064    private void testClientConfiguredGzipContentEncodingAndConnectionReuse(
1065            TransferKind transferKind) throws Exception {
1066        MockResponse responseOne = new MockResponse();
1067        responseOne.addHeader("Content-Encoding: gzip");
1068        transferKind.setBody(responseOne, gzip("one (gzipped)".getBytes("UTF-8")), 5);
1069        server.enqueue(responseOne);
1070        MockResponse responseTwo = new MockResponse();
1071        transferKind.setBody(responseTwo, "two (identity)", 5);
1072        server.enqueue(responseTwo);
1073        server.play();
1074
1075        URLConnection connection = server.getUrl("/").openConnection();
1076        connection.addRequestProperty("Accept-Encoding", "gzip");
1077        InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
1078        assertEquals("one (gzipped)", readAscii(gunzippedIn, Integer.MAX_VALUE));
1079        assertEquals(0, server.takeRequest().getSequenceNumber());
1080
1081        connection = server.getUrl("/").openConnection();
1082        assertEquals("two (identity)", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1083        assertEquals(1, server.takeRequest().getSequenceNumber());
1084    }
1085
1086    /**
1087     * Obnoxiously test that the chunk sizes transmitted exactly equal the
1088     * requested data+chunk header size. Although setChunkedStreamingMode()
1089     * isn't specific about whether the size applies to the data or the
1090     * complete chunk, the RI interprets it as a complete chunk.
1091     */
1092    public void testSetChunkedStreamingMode() throws IOException, InterruptedException {
1093        server.enqueue(new MockResponse());
1094        server.play();
1095
1096        HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
1097        urlConnection.setChunkedStreamingMode(8);
1098        urlConnection.setDoOutput(true);
1099        OutputStream outputStream = urlConnection.getOutputStream();
1100        outputStream.write("ABCDEFGHIJKLMNOPQ".getBytes("US-ASCII"));
1101        assertEquals(200, urlConnection.getResponseCode());
1102
1103        RecordedRequest request = server.takeRequest();
1104        assertEquals("ABCDEFGHIJKLMNOPQ", new String(request.getBody(), "US-ASCII"));
1105        assertEquals(Arrays.asList(3, 3, 3, 3, 3, 2), request.getChunkSizes());
1106    }
1107
1108    public void testAuthenticateWithFixedLengthStreaming() throws Exception {
1109        testAuthenticateWithStreamingPost(StreamingMode.FIXED_LENGTH);
1110    }
1111
1112    public void testAuthenticateWithChunkedStreaming() throws Exception {
1113        testAuthenticateWithStreamingPost(StreamingMode.CHUNKED);
1114    }
1115
1116    private void testAuthenticateWithStreamingPost(StreamingMode streamingMode) throws Exception {
1117        MockResponse pleaseAuthenticate = new MockResponse()
1118                .setResponseCode(401)
1119                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
1120                .setBody("Please authenticate.");
1121        server.enqueue(pleaseAuthenticate);
1122        server.play();
1123
1124        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
1125        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
1126        connection.setDoOutput(true);
1127        byte[] requestBody = { 'A', 'B', 'C', 'D' };
1128        if (streamingMode == StreamingMode.FIXED_LENGTH) {
1129            connection.setFixedLengthStreamingMode(requestBody.length);
1130        } else if (streamingMode == StreamingMode.CHUNKED) {
1131            connection.setChunkedStreamingMode(0);
1132        }
1133        OutputStream outputStream = connection.getOutputStream();
1134        outputStream.write(requestBody);
1135        outputStream.close();
1136        try {
1137            connection.getInputStream();
1138            fail();
1139        } catch (HttpRetryException expected) {
1140        }
1141
1142        // no authorization header for the request...
1143        RecordedRequest request = server.takeRequest();
1144        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
1145        assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
1146    }
1147
1148    enum StreamingMode {
1149        FIXED_LENGTH, CHUNKED
1150    }
1151
1152    public void testAuthenticateWithPost() throws Exception {
1153        MockResponse pleaseAuthenticate = new MockResponse()
1154                .setResponseCode(401)
1155                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
1156                .setBody("Please authenticate.");
1157        // fail auth three times...
1158        server.enqueue(pleaseAuthenticate);
1159        server.enqueue(pleaseAuthenticate);
1160        server.enqueue(pleaseAuthenticate);
1161        // ...then succeed the fourth time
1162        server.enqueue(new MockResponse().setBody("Successful auth!"));
1163        server.play();
1164
1165        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
1166        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
1167        connection.setDoOutput(true);
1168        byte[] requestBody = { 'A', 'B', 'C', 'D' };
1169        OutputStream outputStream = connection.getOutputStream();
1170        outputStream.write(requestBody);
1171        outputStream.close();
1172        assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1173
1174        // no authorization header for the first request...
1175        RecordedRequest request = server.takeRequest();
1176        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
1177
1178        // ...but the three requests that follow include an authorization header
1179        for (int i = 0; i < 3; i++) {
1180            request = server.takeRequest();
1181            assertEquals("POST / HTTP/1.1", request.getRequestLine());
1182            assertContains(request.getHeaders(), "Authorization: Basic "
1183                    + "dXNlcm5hbWU6cGFzc3dvcmQ="); // "dXNl..." == base64("username:password")
1184            assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
1185        }
1186    }
1187
1188    public void testAuthenticateWithGet() throws Exception {
1189        MockResponse pleaseAuthenticate = new MockResponse()
1190                .setResponseCode(401)
1191                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
1192                .setBody("Please authenticate.");
1193        // fail auth three times...
1194        server.enqueue(pleaseAuthenticate);
1195        server.enqueue(pleaseAuthenticate);
1196        server.enqueue(pleaseAuthenticate);
1197        // ...then succeed the fourth time
1198        server.enqueue(new MockResponse().setBody("Successful auth!"));
1199        server.play();
1200
1201        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
1202        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
1203        assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1204
1205        // no authorization header for the first request...
1206        RecordedRequest request = server.takeRequest();
1207        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
1208
1209        // ...but the three requests that follow requests include an authorization header
1210        for (int i = 0; i < 3; i++) {
1211            request = server.takeRequest();
1212            assertEquals("GET / HTTP/1.1", request.getRequestLine());
1213            assertContains(request.getHeaders(), "Authorization: Basic "
1214                    + "dXNlcm5hbWU6cGFzc3dvcmQ="); // "dXNl..." == base64("username:password")
1215        }
1216    }
1217
1218    public void testRedirectedWithChunkedEncoding() throws Exception {
1219        testRedirected(TransferKind.CHUNKED, true);
1220    }
1221
1222    public void testRedirectedWithContentLengthHeader() throws Exception {
1223        testRedirected(TransferKind.FIXED_LENGTH, true);
1224    }
1225
1226    public void testRedirectedWithNoLengthHeaders() throws Exception {
1227        testRedirected(TransferKind.END_OF_STREAM, false);
1228    }
1229
1230    private void testRedirected(TransferKind transferKind, boolean reuse) throws Exception {
1231        MockResponse response = new MockResponse()
1232                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1233                .addHeader("Location: /foo");
1234        transferKind.setBody(response, "This page has moved!", 10);
1235        server.enqueue(response);
1236        server.enqueue(new MockResponse().setBody("This is the new location!"));
1237        server.play();
1238
1239        URLConnection connection = server.getUrl("/").openConnection();
1240        assertEquals("This is the new location!",
1241                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1242
1243        RecordedRequest first = server.takeRequest();
1244        assertEquals("GET / HTTP/1.1", first.getRequestLine());
1245        RecordedRequest retry = server.takeRequest();
1246        assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
1247        if (reuse) {
1248            assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
1249        }
1250    }
1251
1252    public void testRedirectedOnHttps() throws IOException, InterruptedException {
1253        TestSSLContext testSSLContext = TestSSLContext.create();
1254        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
1255        server.enqueue(new MockResponse()
1256                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1257                .addHeader("Location: /foo")
1258                .setBody("This page has moved!"));
1259        server.enqueue(new MockResponse().setBody("This is the new location!"));
1260        server.play();
1261
1262        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
1263        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
1264        assertEquals("This is the new location!",
1265                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1266
1267        RecordedRequest first = server.takeRequest();
1268        assertEquals("GET / HTTP/1.1", first.getRequestLine());
1269        RecordedRequest retry = server.takeRequest();
1270        assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
1271        assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
1272    }
1273
1274    public void testNotRedirectedFromHttpsToHttp() throws IOException, InterruptedException {
1275        TestSSLContext testSSLContext = TestSSLContext.create();
1276        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
1277        server.enqueue(new MockResponse()
1278                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1279                .addHeader("Location: http://anyhost/foo")
1280                .setBody("This page has moved!"));
1281        server.play();
1282
1283        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
1284        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
1285        assertEquals("This page has moved!",
1286                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1287    }
1288
1289    public void testNotRedirectedFromHttpToHttps() throws IOException, InterruptedException {
1290        server.enqueue(new MockResponse()
1291                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1292                .addHeader("Location: https://anyhost/foo")
1293                .setBody("This page has moved!"));
1294        server.play();
1295
1296        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
1297        assertEquals("This page has moved!",
1298                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1299    }
1300
1301    public void testRedirectToAnotherOriginServer() throws Exception {
1302        MockWebServer server2 = new MockWebServer();
1303        server2.enqueue(new MockResponse().setBody("This is the 2nd server!"));
1304        server2.play();
1305
1306        server.enqueue(new MockResponse()
1307                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1308                .addHeader("Location: " + server2.getUrl("/").toString())
1309                .setBody("This page has moved!"));
1310        server.enqueue(new MockResponse().setBody("This is the first server again!"));
1311        server.play();
1312
1313        URLConnection connection = server.getUrl("/").openConnection();
1314        assertEquals("This is the 2nd server!",
1315                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1316        assertEquals(server2.getUrl("/"), connection.getURL());
1317
1318        // make sure the first server was careful to recycle the connection
1319        assertEquals("This is the first server again!",
1320                readAscii(server.getUrl("/").openStream(), Integer.MAX_VALUE));
1321
1322        RecordedRequest first = server.takeRequest();
1323        assertContains(first.getHeaders(), "Host: " + hostname + ":" + server.getPort());
1324        RecordedRequest second = server2.takeRequest();
1325        assertContains(second.getHeaders(), "Host: " + hostname + ":" + server2.getPort());
1326        RecordedRequest third = server.takeRequest();
1327        assertEquals("Expected connection reuse", 1, third.getSequenceNumber());
1328
1329        server2.shutdown();
1330    }
1331
1332    public void testHttpsWithCustomTrustManager() throws Exception {
1333        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
1334        RecordingTrustManager trustManager = new RecordingTrustManager();
1335        SSLContext sc = SSLContext.getInstance("TLS");
1336        sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());
1337
1338        HostnameVerifier defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
1339        HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
1340        SSLSocketFactory defaultSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
1341        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
1342        try {
1343            TestSSLContext testSSLContext = TestSSLContext.create();
1344            server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
1345            server.enqueue(new MockResponse().setBody("ABC"));
1346            server.enqueue(new MockResponse().setBody("DEF"));
1347            server.enqueue(new MockResponse().setBody("GHI"));
1348            server.play();
1349
1350            URL url = server.getUrl("/");
1351            assertEquals("ABC", readAscii(url.openStream(), Integer.MAX_VALUE));
1352            assertEquals("DEF", readAscii(url.openStream(), Integer.MAX_VALUE));
1353            assertEquals("GHI", readAscii(url.openStream(), Integer.MAX_VALUE));
1354
1355            assertEquals(Arrays.asList("verify " + hostname), hostnameVerifier.calls);
1356            assertEquals(Arrays.asList("checkServerTrusted ["
1357                                       + "CN=" + hostname + " 1, "
1358                                       + "CN=Test Intermediate Certificate Authority 1, "
1359                                       + "CN=Test Root Certificate Authority 1"
1360                                       + "] RSA"),
1361                    trustManager.calls);
1362        } finally {
1363            HttpsURLConnection.setDefaultHostnameVerifier(defaultHostnameVerifier);
1364            HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory);
1365        }
1366    }
1367
1368    public void testConnectTimeouts() throws IOException {
1369        // 10.0.0.0 is non-routable and will time out on every network
1370        URLConnection urlConnection = new URL("http://10.0.0.0/").openConnection();
1371        urlConnection.setConnectTimeout(1000);
1372        try {
1373            urlConnection.getInputStream();
1374            fail();
1375        } catch (SocketTimeoutException expected) {
1376        }
1377    }
1378
1379    public void testReadTimeouts() throws IOException {
1380        /*
1381         * This relies on the fact that MockWebServer doesn't close the
1382         * connection after a response has been sent. This causes the client to
1383         * try to read more bytes than are sent, which results in a timeout.
1384         */
1385        MockResponse timeout = new MockResponse()
1386                .setBody("ABC")
1387                .clearHeaders()
1388                .addHeader("Content-Length: 4");
1389        server.enqueue(timeout);
1390        server.play();
1391
1392        URLConnection urlConnection = server.getUrl("/").openConnection();
1393        urlConnection.setReadTimeout(1000);
1394        InputStream in = urlConnection.getInputStream();
1395        assertEquals('A', in.read());
1396        assertEquals('B', in.read());
1397        assertEquals('C', in.read());
1398        try {
1399            in.read(); // if Content-Length was accurate, this would return -1 immediately
1400            fail();
1401        } catch (SocketTimeoutException expected) {
1402        }
1403    }
1404
1405    public void testSetChunkedEncodingAsRequestProperty() throws IOException, InterruptedException {
1406        server.enqueue(new MockResponse());
1407        server.play();
1408
1409        HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
1410        urlConnection.setRequestProperty("Transfer-encoding", "chunked");
1411        urlConnection.setDoOutput(true);
1412        urlConnection.getOutputStream().write("ABC".getBytes("UTF-8"));
1413        assertEquals(200, urlConnection.getResponseCode());
1414
1415        RecordedRequest request = server.takeRequest();
1416        assertEquals("ABC", new String(request.getBody(), "UTF-8"));
1417    }
1418
1419    public void testConnectionCloseInRequest() throws IOException, InterruptedException {
1420        server.enqueue(new MockResponse()); // server doesn't honor the connection: close header!
1421        server.enqueue(new MockResponse());
1422        server.play();
1423
1424        HttpURLConnection a = (HttpURLConnection) server.getUrl("/").openConnection();
1425        a.setRequestProperty("Connection", "close");
1426        assertEquals(200, a.getResponseCode());
1427
1428        HttpURLConnection b = (HttpURLConnection) server.getUrl("/").openConnection();
1429        assertEquals(200, b.getResponseCode());
1430
1431        assertEquals(0, server.takeRequest().getSequenceNumber());
1432        assertEquals("When connection: close is used, each request should get its own connection",
1433                0, server.takeRequest().getSequenceNumber());
1434    }
1435
1436    public void testConnectionCloseInResponse() throws IOException, InterruptedException {
1437        server.enqueue(new MockResponse().addHeader("Connection: close"));
1438        server.enqueue(new MockResponse());
1439        server.play();
1440
1441        HttpURLConnection a = (HttpURLConnection) server.getUrl("/").openConnection();
1442        assertEquals(200, a.getResponseCode());
1443
1444        HttpURLConnection b = (HttpURLConnection) server.getUrl("/").openConnection();
1445        assertEquals(200, b.getResponseCode());
1446
1447        assertEquals(0, server.takeRequest().getSequenceNumber());
1448        assertEquals("When connection: close is used, each request should get its own connection",
1449                0, server.takeRequest().getSequenceNumber());
1450    }
1451
1452    public void testConnectionCloseWithRedirect() throws IOException, InterruptedException {
1453        MockResponse response = new MockResponse()
1454                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1455                .addHeader("Location: /foo")
1456                .addHeader("Connection: close");
1457        server.enqueue(response);
1458        server.enqueue(new MockResponse().setBody("This is the new location!"));
1459        server.play();
1460
1461        URLConnection connection = server.getUrl("/").openConnection();
1462        assertEquals("This is the new location!",
1463                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1464
1465        assertEquals(0, server.takeRequest().getSequenceNumber());
1466        assertEquals("When connection: close is used, each request should get its own connection",
1467                0, server.takeRequest().getSequenceNumber());
1468    }
1469
1470    public void testResponseCodeDisagreesWithHeaders() throws IOException, InterruptedException {
1471        server.enqueue(new MockResponse()
1472                .setResponseCode(HttpURLConnection.HTTP_NO_CONTENT)
1473                .setBody("This body is not allowed!"));
1474        server.play();
1475
1476        URLConnection connection = server.getUrl("/").openConnection();
1477        assertEquals("This body is not allowed!",
1478                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1479    }
1480
1481    public void testSingleByteReadIsSigned() throws IOException {
1482        server.enqueue(new MockResponse().setBody(new byte[] { -2, -1 }));
1483        server.play();
1484
1485        URLConnection connection = server.getUrl("/").openConnection();
1486        InputStream in = connection.getInputStream();
1487        assertEquals(254, in.read());
1488        assertEquals(255, in.read());
1489        assertEquals(-1, in.read());
1490    }
1491
1492    public void testFlushAfterStreamTransmittedWithChunkedEncoding() throws IOException {
1493        testFlushAfterStreamTransmitted(TransferKind.CHUNKED);
1494    }
1495
1496    public void testFlushAfterStreamTransmittedWithFixedLength() throws IOException {
1497        testFlushAfterStreamTransmitted(TransferKind.FIXED_LENGTH);
1498    }
1499
1500    public void testFlushAfterStreamTransmittedWithNoLengthHeaders() throws IOException {
1501        testFlushAfterStreamTransmitted(TransferKind.END_OF_STREAM);
1502    }
1503
1504    /**
1505     * We explicitly permit apps to close the upload stream even after it has
1506     * been transmitted.  We also permit flush so that buffered streams can
1507     * do a no-op flush when they are closed. http://b/3038470
1508     */
1509    private void testFlushAfterStreamTransmitted(TransferKind transferKind) throws IOException {
1510        server.enqueue(new MockResponse().setBody("abc"));
1511        server.play();
1512
1513        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
1514        connection.setDoOutput(true);
1515        byte[] upload = "def".getBytes("UTF-8");
1516
1517        if (transferKind == TransferKind.CHUNKED) {
1518            connection.setChunkedStreamingMode(0);
1519        } else if (transferKind == TransferKind.FIXED_LENGTH) {
1520            connection.setFixedLengthStreamingMode(upload.length);
1521        }
1522
1523        OutputStream out = connection.getOutputStream();
1524        out.write(upload);
1525        assertEquals("abc", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1526
1527        out.flush(); // dubious but permitted
1528        try {
1529            out.write("ghi".getBytes("UTF-8"));
1530            fail();
1531        } catch (IOException expected) {
1532        }
1533    }
1534
1535    /**
1536     * Encodes the response body using GZIP and adds the corresponding header.
1537     */
1538    public byte[] gzip(byte[] bytes) throws IOException {
1539        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
1540        OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
1541        gzippedOut.write(bytes);
1542        gzippedOut.close();
1543        return bytesOut.toByteArray();
1544    }
1545
1546    private <T> List<T> toListOrNull(T[] arrayOrNull) {
1547        return arrayOrNull != null ? Arrays.asList(arrayOrNull) : null;
1548    }
1549
1550    /**
1551     * Reads at most {@code limit} characters from {@code in} and asserts that
1552     * content equals {@code expected}.
1553     */
1554    private void assertContent(String expected, URLConnection connection, int limit)
1555            throws IOException {
1556        assertEquals(expected, readAscii(connection.getInputStream(), limit));
1557        ((HttpURLConnection) connection).disconnect();
1558    }
1559
1560    private void assertContent(String expected, URLConnection connection) throws IOException {
1561        assertContent(expected, connection, Integer.MAX_VALUE);
1562    }
1563
1564    private void assertContains(List<String> headers, String header) {
1565        assertTrue(headers.toString(), headers.contains(header));
1566    }
1567
1568    private void assertContainsNoneMatching(List<String> headers, String pattern) {
1569        for (String header : headers) {
1570            if (header.matches(pattern)) {
1571                fail("Header " + header + " matches " + pattern);
1572            }
1573        }
1574    }
1575
1576    private Set<String> newSet(String... elements) {
1577        return new HashSet<String>(Arrays.asList(elements));
1578    }
1579
1580    enum TransferKind {
1581        CHUNKED() {
1582            @Override void setBody(MockResponse response, byte[] content, int chunkSize)
1583                    throws IOException {
1584                response.setChunkedBody(content, chunkSize);
1585            }
1586        },
1587        FIXED_LENGTH() {
1588            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
1589                response.setBody(content);
1590            }
1591        },
1592        END_OF_STREAM() {
1593            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
1594                response.setBody(content);
1595                response.setDisconnectAtEnd(true);
1596                for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
1597                    if (h.next().startsWith("Content-Length:")) {
1598                        h.remove();
1599                        break;
1600                    }
1601                }
1602            }
1603        };
1604
1605        abstract void setBody(MockResponse response, byte[] content, int chunkSize)
1606                throws IOException;
1607
1608        void setBody(MockResponse response, String content, int chunkSize) throws IOException {
1609            setBody(response, content.getBytes("UTF-8"), chunkSize);
1610        }
1611    }
1612
1613    enum ProxyConfig {
1614        CREATE_ARG() {
1615            @Override public HttpURLConnection connect(MockWebServer server, URL url)
1616                    throws IOException {
1617                return (HttpURLConnection) url.openConnection(server.toProxyAddress());
1618            }
1619        },
1620
1621        PROXY_SYSTEM_PROPERTY() {
1622            @Override public HttpURLConnection connect(MockWebServer server, URL url)
1623                    throws IOException {
1624                System.setProperty("proxyHost", "localhost");
1625                System.setProperty("proxyPort", Integer.toString(server.getPort()));
1626                return (HttpURLConnection) url.openConnection();
1627            }
1628        },
1629
1630        HTTP_PROXY_SYSTEM_PROPERTY() {
1631            @Override public HttpURLConnection connect(MockWebServer server, URL url)
1632                    throws IOException {
1633                System.setProperty("http.proxyHost", "localhost");
1634                System.setProperty("http.proxyPort", Integer.toString(server.getPort()));
1635                return (HttpURLConnection) url.openConnection();
1636            }
1637        },
1638
1639        HTTPS_PROXY_SYSTEM_PROPERTY() {
1640            @Override public HttpURLConnection connect(MockWebServer server, URL url)
1641                    throws IOException {
1642                System.setProperty("https.proxyHost", "localhost");
1643                System.setProperty("https.proxyPort", Integer.toString(server.getPort()));
1644                return (HttpURLConnection) url.openConnection();
1645            }
1646        };
1647
1648        public abstract HttpURLConnection connect(MockWebServer server, URL url) throws IOException;
1649    }
1650
1651    private static class RecordingTrustManager implements X509TrustManager {
1652        private final List<String> calls = new ArrayList<String>();
1653
1654        public X509Certificate[] getAcceptedIssuers() {
1655            calls.add("getAcceptedIssuers");
1656            return new X509Certificate[] {};
1657        }
1658
1659        public void checkClientTrusted(X509Certificate[] chain, String authType)
1660                throws CertificateException {
1661            calls.add("checkClientTrusted " + certificatesToString(chain) + " " + authType);
1662        }
1663
1664        public void checkServerTrusted(X509Certificate[] chain, String authType)
1665                throws CertificateException {
1666            calls.add("checkServerTrusted " + certificatesToString(chain) + " " + authType);
1667        }
1668
1669        private String certificatesToString(X509Certificate[] certificates) {
1670            List<String> result = new ArrayList<String>();
1671            for (X509Certificate certificate : certificates) {
1672                result.add(certificate.getSubjectDN() + " " + certificate.getSerialNumber());
1673            }
1674            return result.toString();
1675        }
1676    }
1677
1678    private static class RecordingHostnameVerifier implements HostnameVerifier {
1679        private final List<String> calls = new ArrayList<String>();
1680
1681        public boolean verify(String hostname, SSLSession session) {
1682            calls.add("verify " + hostname);
1683            return true;
1684        }
1685    }
1686
1687    private static class InsecureResponseCache extends ResponseCache {
1688        private final DefaultResponseCache delegate = new DefaultResponseCache();
1689
1690        @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
1691            return delegate.put(uri, connection);
1692        }
1693
1694        @Override public CacheResponse get(URI uri, String requestMethod,
1695                Map<String, List<String>> requestHeaders) throws IOException {
1696            final CacheResponse response = delegate.get(uri, requestMethod, requestHeaders);
1697            if (response instanceof SecureCacheResponse) {
1698                return new CacheResponse() {
1699                    @Override public InputStream getBody() throws IOException {
1700                        return response.getBody();
1701                    }
1702                    @Override public Map<String, List<String>> getHeaders() throws IOException {
1703                        return response.getHeaders();
1704                    }
1705                };
1706            }
1707            return response;
1708        }
1709    }
1710}
1711