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