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