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