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