URLConnectionTest.java revision 00feece22909b7dc79fc96d666d157390b93858e
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    public void test_responseCaching_407() throws Exception {
334        // This test will fail on Android because we throw if we're not using a proxy.
335        // This isn't true of the RI, but it seems like useful debugging behavior.
336        assertCached(false, 407);
337    }
338
339    public void test_responseCaching_410() throws Exception {
340        // the HTTP spec permits caching 410s, but the RI doesn't.
341        assertCached(false, 410);
342    }
343
344    private void assertCached(boolean shouldPut, int responseCode) throws Exception {
345        server = new MockWebServer();
346        server.enqueue(new MockResponse()
347                .setResponseCode(responseCode)
348                .setBody("ABCDE")
349                .addHeader("WWW-Authenticate: challenge"));
350        server.play();
351
352        DefaultResponseCache responseCache = new DefaultResponseCache();
353        ResponseCache.setDefault(responseCache);
354        URL url = server.getUrl("/");
355        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
356        assertEquals(responseCode, conn.getResponseCode());
357
358        // exhaust the content stream
359        try {
360            // TODO: remove special case once testUnauthorizedResponseHandling() is fixed
361            if (responseCode != 401) {
362                readAscii(conn.getInputStream(), Integer.MAX_VALUE);
363            }
364        } catch (IOException ignored) {
365        }
366
367        Set<URI> expectedCachedUris = shouldPut
368                ? Collections.singleton(url.toURI())
369                : Collections.<URI>emptySet();
370        assertEquals(Integer.toString(responseCode),
371                expectedCachedUris, responseCache.getContents().keySet());
372        server.shutdown(); // tearDown() isn't sufficient; this test starts multiple servers
373    }
374
375    public void testConnectViaHttps() throws IOException, InterruptedException {
376        TestSSLContext testSSLContext = TestSSLContext.create();
377
378        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
379        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
380        server.play();
381
382        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/foo").openConnection();
383        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
384
385        assertContent("this response comes via HTTPS", connection);
386
387        RecordedRequest request = server.takeRequest();
388        assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
389    }
390
391    public void testConnectViaHttpsReusingConnections() throws IOException, InterruptedException {
392        TestSSLContext testSSLContext = TestSSLContext.create();
393
394        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
395        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
396        server.enqueue(new MockResponse().setBody("another response via HTTPS"));
397        server.play();
398
399        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
400        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
401        assertContent("this response comes via HTTPS", connection);
402
403        connection = (HttpsURLConnection) server.getUrl("/").openConnection();
404        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
405        assertContent("another response via HTTPS", connection);
406
407        assertEquals(0, server.takeRequest().getSequenceNumber());
408        assertEquals(1, server.takeRequest().getSequenceNumber());
409    }
410
411    public void testConnectViaHttpsReusingConnectionsDifferentFactories()
412            throws IOException, InterruptedException {
413        TestSSLContext testSSLContext = TestSSLContext.create();
414
415        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
416        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
417        server.enqueue(new MockResponse().setBody("another response via HTTPS"));
418        server.play();
419
420        // install a custom SSL socket factory so the server can be authorized
421        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
422        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
423        assertContent("this response comes via HTTPS", connection);
424
425        connection = (HttpsURLConnection) server.getUrl("/").openConnection();
426        try {
427            readAscii(connection.getInputStream(), Integer.MAX_VALUE);
428            fail("without an SSL socket factory, the connection should fail");
429        } catch (SSLException expected) {
430        }
431    }
432
433    public void testConnectViaHttpsWithSSLFallback() throws IOException, InterruptedException {
434        TestSSLContext testSSLContext = TestSSLContext.create();
435
436        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
437        server.enqueue(new MockResponse().setDisconnectAtStart(true));
438        server.enqueue(new MockResponse().setBody("this response comes via SSL"));
439        server.play();
440
441        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/foo").openConnection();
442        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
443
444        assertContent("this response comes via SSL", connection);
445
446        RecordedRequest request = server.takeRequest();
447        assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
448    }
449
450    public void testConnectViaProxy() throws IOException, InterruptedException {
451        MockResponse mockResponse = new MockResponse().setBody("this response comes via a proxy");
452        server.enqueue(mockResponse);
453        server.play();
454
455        URLConnection connection = new URL("http://android.com/foo").openConnection(
456                server.toProxyAddress());
457        assertContent("this response comes via a proxy", connection);
458
459        RecordedRequest request = server.takeRequest();
460        assertEquals("GET http://android.com/foo HTTP/1.1", request.getRequestLine());
461        assertContains(request.getHeaders(), "Host: android.com");
462    }
463
464    public void testContentDisagreesWithContentLengthHeader() throws IOException {
465        server.enqueue(new MockResponse()
466                .setBody("abc\r\nYOU SHOULD NOT SEE THIS")
467                .clearHeaders()
468                .addHeader("Content-Length: 3"));
469        server.play();
470
471        assertContent("abc", server.getUrl("/").openConnection());
472    }
473
474    public void testContentDisagreesWithChunkedHeader() throws IOException {
475        MockResponse mockResponse = new MockResponse();
476        mockResponse.setChunkedBody("abc", 3);
477        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
478        bytesOut.write(mockResponse.getBody());
479        bytesOut.write("\r\nYOU SHOULD NOT SEE THIS".getBytes());
480        mockResponse.setBody(bytesOut.toByteArray());
481        mockResponse.clearHeaders();
482        mockResponse.addHeader("Transfer-encoding: chunked");
483
484        server.enqueue(mockResponse);
485        server.play();
486
487        assertContent("abc", server.getUrl("/").openConnection());
488    }
489
490    public void testConnectViaHttpProxyToHttps() throws IOException, InterruptedException {
491        TestSSLContext testSSLContext = TestSSLContext.create();
492
493        server.useHttps(testSSLContext.serverContext.getSocketFactory(), true);
494        server.enqueue(new MockResponse().clearHeaders()); // for CONNECT
495        server.enqueue(new MockResponse().setBody("this response comes via a secure proxy"));
496        server.play();
497
498        URL url = new URL("https://android.com/foo");
499        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(
500                server.toProxyAddress());
501        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
502        connection.setHostnameVerifier(ALWAYS_TRUST_VERIFIER);
503
504        assertContent("this response comes via a secure proxy", connection);
505
506        RecordedRequest connect = server.takeRequest();
507        assertEquals("Connect line failure on proxy",
508                "CONNECT android.com:443 HTTP/1.1", connect.getRequestLine());
509        assertContains(connect.getHeaders(), "Host: android.com");
510
511        RecordedRequest get = server.takeRequest();
512        assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
513        assertContains(get.getHeaders(), "Host: android.com");
514    }
515
516    /**
517     * Test which headers are sent unencrypted to the HTTP proxy.
518     */
519    public void testProxyConnectIncludesProxyHeadersOnly()
520            throws IOException, InterruptedException {
521        TestSSLContext testSSLContext = TestSSLContext.create();
522
523        server.useHttps(testSSLContext.serverContext.getSocketFactory(), true);
524        server.enqueue(new MockResponse().clearHeaders()); // for CONNECT
525        server.enqueue(new MockResponse().setBody("encrypted response from the origin server"));
526        server.play();
527
528        URL url = new URL("https://android.com/foo");
529        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(
530                server.toProxyAddress());
531        connection.addRequestProperty("Private", "Secret");
532        connection.addRequestProperty("Proxy-Authorization", "bar");
533        connection.addRequestProperty("User-Agent", "baz");
534        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
535        connection.setHostnameVerifier(ALWAYS_TRUST_VERIFIER);
536        assertContent("encrypted response from the origin server", connection);
537
538        RecordedRequest connect = server.takeRequest();
539        assertContainsNoneMatching(connect.getHeaders(), "Private.*");
540        assertContains(connect.getHeaders(), "Proxy-Authorization: bar");
541        assertContains(connect.getHeaders(), "User-Agent: baz");
542        assertContains(connect.getHeaders(), "Host: android.com");
543        assertContains(connect.getHeaders(), "Proxy-Connection: Keep-Alive");
544
545        RecordedRequest get = server.takeRequest();
546        assertContains(get.getHeaders(), "Private: Secret");
547    }
548
549    public void testDisconnectedConnection() throws IOException {
550        server.enqueue(new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"));
551        server.play();
552
553        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
554        InputStream in = connection.getInputStream();
555        assertEquals('A', (char) in.read());
556        connection.disconnect();
557        try {
558            in.read();
559            fail("Expected a connection closed exception");
560        } catch (IOException expected) {
561        }
562    }
563
564    public void testResponseCachingAndInputStreamSkipWithFixedLength() throws IOException {
565        testResponseCaching(TransferKind.FIXED_LENGTH);
566    }
567
568    public void testResponseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException {
569        testResponseCaching(TransferKind.CHUNKED);
570    }
571
572    public void testResponseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException {
573        testResponseCaching(TransferKind.END_OF_STREAM);
574    }
575
576    /**
577     * HttpURLConnection.getInputStream().skip(long) causes ResponseCache corruption
578     * http://code.google.com/p/android/issues/detail?id=8175
579     */
580    private void testResponseCaching(TransferKind transferKind) throws IOException {
581        MockResponse response = new MockResponse();
582        transferKind.setBody(response, "I love puppies but hate spiders", 1);
583        server.enqueue(response);
584        server.play();
585
586        DefaultResponseCache cache = new DefaultResponseCache();
587        ResponseCache.setDefault(cache);
588
589        // Make sure that calling skip() doesn't omit bytes from the cache.
590        URLConnection urlConnection = server.getUrl("/").openConnection();
591        InputStream in = urlConnection.getInputStream();
592        assertEquals("I love ", readAscii(in, "I love ".length()));
593        reliableSkip(in, "puppies but hate ".length());
594        assertEquals("spiders", readAscii(in, "spiders".length()));
595        assertEquals(-1, in.read());
596        in.close();
597        assertEquals(1, cache.getSuccessCount());
598        assertEquals(0, cache.getAbortCount());
599
600        urlConnection = server.getUrl("/").openConnection(); // this response is cached!
601        in = urlConnection.getInputStream();
602        assertEquals("I love puppies but hate spiders",
603                readAscii(in, "I love puppies but hate spiders".length()));
604        assertEquals(-1, in.read());
605        assertEquals(1, cache.getMissCount());
606        assertEquals(1, cache.getHitCount());
607        assertEquals(1, cache.getSuccessCount());
608        assertEquals(0, cache.getAbortCount());
609    }
610
611    public void testResponseCacheRequestHeaders() throws IOException, URISyntaxException {
612        server.enqueue(new MockResponse().setBody("ABC"));
613        server.play();
614
615        final AtomicReference<Map<String, List<String>>> requestHeadersRef
616                = new AtomicReference<Map<String, List<String>>>();
617        ResponseCache.setDefault(new ResponseCache() {
618            @Override public CacheResponse get(URI uri, String requestMethod,
619                    Map<String, List<String>> requestHeaders) throws IOException {
620                requestHeadersRef.set(requestHeaders);
621                return null;
622            }
623            @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException {
624                return null;
625            }
626        });
627
628        URL url = server.getUrl("/");
629        URLConnection urlConnection = url.openConnection();
630        urlConnection.addRequestProperty("A", "android");
631        readAscii(urlConnection.getInputStream(), Integer.MAX_VALUE);
632        assertEquals(Arrays.asList("android"), requestHeadersRef.get().get("A"));
633    }
634
635    private void reliableSkip(InputStream in, int length) throws IOException {
636        while (length > 0) {
637            length -= in.skip(length);
638        }
639    }
640
641    /**
642     * Reads {@code count} characters from the stream. If the stream is
643     * exhausted before {@code count} characters can be read, the remaining
644     * characters are returned and the stream is closed.
645     */
646    private String readAscii(InputStream in, int count) throws IOException {
647        StringBuilder result = new StringBuilder();
648        for (int i = 0; i < count; i++) {
649            int value = in.read();
650            if (value == -1) {
651                in.close();
652                break;
653            }
654            result.append((char) value);
655        }
656        return result.toString();
657    }
658
659    public void testServerDisconnectsPrematurelyWithContentLengthHeader() throws IOException {
660        testServerPrematureDisconnect(TransferKind.FIXED_LENGTH);
661    }
662
663    public void testServerDisconnectsPrematurelyWithChunkedEncoding() throws IOException {
664        testServerPrematureDisconnect(TransferKind.CHUNKED);
665    }
666
667    public void testServerDisconnectsPrematurelyWithNoLengthHeaders() throws IOException {
668        /*
669         * Intentionally empty. This case doesn't make sense because there's no
670         * such thing as a premature disconnect when the disconnect itself
671         * indicates the end of the data stream.
672         */
673    }
674
675    private void testServerPrematureDisconnect(TransferKind transferKind) throws IOException {
676        MockResponse response = new MockResponse();
677        transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16);
678        server.enqueue(truncateViolently(response, 16));
679        server.enqueue(new MockResponse().setBody("Request #2"));
680        server.play();
681
682        DefaultResponseCache cache = new DefaultResponseCache();
683        ResponseCache.setDefault(cache);
684
685        BufferedReader reader = new BufferedReader(new InputStreamReader(
686                server.getUrl("/").openConnection().getInputStream()));
687        assertEquals("ABCDE", reader.readLine());
688        try {
689            reader.readLine();
690            fail("This implementation silently ignored a truncated HTTP body.");
691        } catch (IOException expected) {
692        }
693
694        assertEquals(1, cache.getAbortCount());
695        assertEquals(0, cache.getSuccessCount());
696        assertContent("Request #2", server.getUrl("/").openConnection());
697        assertEquals(1, cache.getAbortCount());
698        assertEquals(1, cache.getSuccessCount());
699    }
700
701    public void testClientPrematureDisconnectWithContentLengthHeader() throws IOException {
702        testClientPrematureDisconnect(TransferKind.FIXED_LENGTH);
703    }
704
705    public void testClientPrematureDisconnectWithChunkedEncoding() throws IOException {
706        testClientPrematureDisconnect(TransferKind.CHUNKED);
707    }
708
709    public void testClientPrematureDisconnectWithNoLengthHeaders() throws IOException {
710        testClientPrematureDisconnect(TransferKind.END_OF_STREAM);
711    }
712
713    private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException {
714        MockResponse response = new MockResponse();
715        transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024);
716        server.enqueue(response);
717        server.enqueue(new MockResponse().setBody("Request #2"));
718        server.play();
719
720        DefaultResponseCache cache = new DefaultResponseCache();
721        ResponseCache.setDefault(cache);
722
723        InputStream in = server.getUrl("/").openConnection().getInputStream();
724        assertEquals("ABCDE", readAscii(in, 5));
725        in.close();
726        try {
727            in.read();
728            fail("Expected an IOException because the stream is closed.");
729        } catch (IOException expected) {
730        }
731
732        assertEquals(1, cache.getAbortCount());
733        assertEquals(0, cache.getSuccessCount());
734        assertContent("Request #2", server.getUrl("/").openConnection());
735        assertEquals(1, cache.getAbortCount());
736        assertEquals(1, cache.getSuccessCount());
737    }
738
739    /**
740     * Shortens the body of {@code response} but not the corresponding headers.
741     * Only useful to test how clients respond to the premature conclusion of
742     * the HTTP body.
743     */
744    private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) {
745        response.setDisconnectAtEnd(true);
746        List<String> headers = new ArrayList<String>(response.getHeaders());
747        response.setBody(Arrays.copyOfRange(response.getBody(), 0, numBytesToKeep));
748        response.getHeaders().clear();
749        response.getHeaders().addAll(headers);
750        return response;
751    }
752
753    public void testMarkAndResetWithContentLengthHeader() throws IOException {
754        testMarkAndReset(TransferKind.FIXED_LENGTH);
755    }
756
757    public void testMarkAndResetWithChunkedEncoding() throws IOException {
758        testMarkAndReset(TransferKind.CHUNKED);
759    }
760
761    public void testMarkAndResetWithNoLengthHeaders() throws IOException {
762        testMarkAndReset(TransferKind.END_OF_STREAM);
763    }
764
765    public void testMarkAndReset(TransferKind transferKind) throws IOException {
766        MockResponse response = new MockResponse();
767        transferKind.setBody(response, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1024);
768        server.enqueue(response);
769        server.play();
770
771        DefaultResponseCache cache = new DefaultResponseCache();
772        ResponseCache.setDefault(cache);
773
774        InputStream in = server.getUrl("/").openConnection().getInputStream();
775        assertFalse("This implementation claims to support mark().", in.markSupported());
776        in.mark(5);
777        assertEquals("ABCDE", readAscii(in, 5));
778        try {
779            in.reset();
780            fail();
781        } catch (IOException expected) {
782        }
783        assertEquals("FGHIJKLMNOPQRSTUVWXYZ", readAscii(in, Integer.MAX_VALUE));
784
785        assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", server.getUrl("/").openConnection());
786        assertEquals(1, cache.getSuccessCount());
787        assertEquals(1, cache.getHitCount());
788    }
789
790    /**
791     * We've had a bug where we forget the HTTP response when we see response
792     * code 401. This causes a new HTTP request to be issued for every call into
793     * the URLConnection.
794     */
795    public void testUnauthorizedResponseHandling() throws IOException {
796        MockResponse response = new MockResponse()
797                .addHeader("WWW-Authenticate: challenge")
798                .setResponseCode(401) // UNAUTHORIZED
799                .setBody("Unauthorized");
800        server.enqueue(response);
801        server.enqueue(response);
802        server.enqueue(response);
803        server.play();
804
805        URL url = server.getUrl("/");
806        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
807
808        assertEquals(401, conn.getResponseCode());
809        assertEquals(401, conn.getResponseCode());
810        assertEquals(401, conn.getResponseCode());
811        assertEquals(1, server.getRequestCount());
812    }
813
814    public void testNonHexChunkSize() throws IOException {
815        server.enqueue(new MockResponse()
816                .setBody("5\r\nABCDE\r\nG\r\nFGHIJKLMNOPQRSTU\r\n0\r\n\r\n")
817                .clearHeaders()
818                .addHeader("Transfer-encoding: chunked"));
819        server.play();
820
821        URLConnection connection = server.getUrl("/").openConnection();
822        try {
823            readAscii(connection.getInputStream(), Integer.MAX_VALUE);
824            fail();
825        } catch (IOException e) {
826        }
827    }
828
829    public void testMissingChunkBody() throws IOException {
830        server.enqueue(new MockResponse()
831                .setBody("5")
832                .clearHeaders()
833                .addHeader("Transfer-encoding: chunked")
834                .setDisconnectAtEnd(true));
835        server.play();
836
837        URLConnection connection = server.getUrl("/").openConnection();
838        try {
839            readAscii(connection.getInputStream(), Integer.MAX_VALUE);
840            fail();
841        } catch (IOException e) {
842        }
843    }
844
845    /**
846     * This test checks whether connections are gzipped by default. This
847     * behavior in not required by the API, so a failure of this test does not
848     * imply a bug in the implementation.
849     */
850    public void testGzipEncodingEnabledByDefault() throws IOException, InterruptedException {
851        server.enqueue(new MockResponse()
852                .setBody(gzip("ABCABCABC".getBytes("UTF-8")))
853                .addHeader("Content-Encoding: gzip"));
854        server.play();
855
856        URLConnection connection = server.getUrl("/").openConnection();
857        assertEquals("ABCABCABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
858        assertNull(connection.getContentEncoding());
859
860        RecordedRequest request = server.takeRequest();
861        assertContains(request.getHeaders(), "Accept-Encoding: gzip");
862    }
863
864    public void testClientConfiguredGzipContentEncoding() throws Exception {
865        server.enqueue(new MockResponse()
866                .setBody(gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8")))
867                .addHeader("Content-Encoding: gzip"));
868        server.play();
869
870        URLConnection connection = server.getUrl("/").openConnection();
871        connection.addRequestProperty("Accept-Encoding", "gzip");
872        InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
873        assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", readAscii(gunzippedIn, Integer.MAX_VALUE));
874
875        RecordedRequest request = server.takeRequest();
876        assertContains(request.getHeaders(), "Accept-Encoding: gzip");
877    }
878
879    public void testGzipAndConnectionReuseWithFixedLength() throws Exception {
880        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH);
881    }
882
883    public void testGzipAndConnectionReuseWithChunkedEncoding() throws Exception {
884        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED);
885    }
886
887    public void testClientConfiguredCustomContentEncoding() throws Exception {
888        server.enqueue(new MockResponse()
889                .setBody("ABCDE")
890                .addHeader("Content-Encoding: custom"));
891        server.play();
892
893        URLConnection connection = server.getUrl("/").openConnection();
894        connection.addRequestProperty("Accept-Encoding", "custom");
895        assertEquals("ABCDE", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
896
897        RecordedRequest request = server.takeRequest();
898        assertContains(request.getHeaders(), "Accept-Encoding: custom");
899    }
900
901    /**
902     * Test a bug where gzip input streams weren't exhausting the input stream,
903     * which corrupted the request that followed.
904     * http://code.google.com/p/android/issues/detail?id=7059
905     */
906    private void testClientConfiguredGzipContentEncodingAndConnectionReuse(
907            TransferKind transferKind) throws Exception {
908        MockResponse responseOne = new MockResponse();
909        responseOne.addHeader("Content-Encoding: gzip");
910        transferKind.setBody(responseOne, gzip("one (gzipped)".getBytes("UTF-8")), 5);
911        server.enqueue(responseOne);
912        MockResponse responseTwo = new MockResponse();
913        transferKind.setBody(responseTwo, "two (identity)", 5);
914        server.enqueue(responseTwo);
915        server.play();
916
917        URLConnection connection = server.getUrl("/").openConnection();
918        connection.addRequestProperty("Accept-Encoding", "gzip");
919        InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
920        assertEquals("one (gzipped)", readAscii(gunzippedIn, Integer.MAX_VALUE));
921        assertEquals(0, server.takeRequest().getSequenceNumber());
922
923        connection = server.getUrl("/").openConnection();
924        assertEquals("two (identity)", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
925        assertEquals(1, server.takeRequest().getSequenceNumber());
926    }
927
928    /**
929     * Obnoxiously test that the chunk sizes transmitted exactly equal the
930     * requested data+chunk header size. Although setChunkedStreamingMode()
931     * isn't specific about whether the size applies to the data or the
932     * complete chunk, the RI interprets it as a complete chunk.
933     */
934    public void testSetChunkedStreamingMode() throws IOException, InterruptedException {
935        server.enqueue(new MockResponse());
936        server.play();
937
938        HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
939        urlConnection.setChunkedStreamingMode(8);
940        urlConnection.setDoOutput(true);
941        OutputStream outputStream = urlConnection.getOutputStream();
942        outputStream.write("ABCDEFGHIJKLMNOPQ".getBytes("US-ASCII"));
943        assertEquals(200, urlConnection.getResponseCode());
944
945        RecordedRequest request = server.takeRequest();
946        assertEquals("ABCDEFGHIJKLMNOPQ", new String(request.getBody(), "US-ASCII"));
947        assertEquals(Arrays.asList(3, 3, 3, 3, 3, 2), request.getChunkSizes());
948    }
949
950    public void testAuthenticateWithFixedLengthStreaming() throws Exception {
951        testAuthenticateWithStreamingPost(StreamingMode.FIXED_LENGTH);
952    }
953
954    public void testAuthenticateWithChunkedStreaming() throws Exception {
955        testAuthenticateWithStreamingPost(StreamingMode.CHUNKED);
956    }
957
958    private void testAuthenticateWithStreamingPost(StreamingMode streamingMode) throws Exception {
959        MockResponse pleaseAuthenticate = new MockResponse()
960                .setResponseCode(401)
961                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
962                .setBody("Please authenticate.");
963        server.enqueue(pleaseAuthenticate);
964        server.play();
965
966        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
967        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
968        connection.setDoOutput(true);
969        byte[] requestBody = { 'A', 'B', 'C', 'D' };
970        if (streamingMode == StreamingMode.FIXED_LENGTH) {
971            connection.setFixedLengthStreamingMode(requestBody.length);
972        } else if (streamingMode == StreamingMode.CHUNKED) {
973            connection.setChunkedStreamingMode(0);
974        }
975        OutputStream outputStream = connection.getOutputStream();
976        outputStream.write(requestBody);
977        outputStream.close();
978        try {
979            connection.getInputStream();
980            fail();
981        } catch (HttpRetryException expected) {
982        }
983
984        // no authorization header for the request...
985        RecordedRequest request = server.takeRequest();
986        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
987        assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
988    }
989
990    enum StreamingMode {
991        FIXED_LENGTH, CHUNKED
992    }
993
994    public void testAuthenticateWithPost() throws Exception {
995        MockResponse pleaseAuthenticate = new MockResponse()
996                .setResponseCode(401)
997                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
998                .setBody("Please authenticate.");
999        // fail auth three times...
1000        server.enqueue(pleaseAuthenticate);
1001        server.enqueue(pleaseAuthenticate);
1002        server.enqueue(pleaseAuthenticate);
1003        // ...then succeed the fourth time
1004        server.enqueue(new MockResponse().setBody("Successful auth!"));
1005        server.play();
1006
1007        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
1008        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
1009        connection.setDoOutput(true);
1010        byte[] requestBody = { 'A', 'B', 'C', 'D' };
1011        OutputStream outputStream = connection.getOutputStream();
1012        outputStream.write(requestBody);
1013        outputStream.close();
1014        assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1015
1016        // no authorization header for the first request...
1017        RecordedRequest request = server.takeRequest();
1018        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
1019
1020        // ...but the three requests that follow include an authorization header
1021        for (int i = 0; i < 3; i++) {
1022            request = server.takeRequest();
1023            assertEquals("POST / HTTP/1.1", request.getRequestLine());
1024            assertContains(request.getHeaders(), "Authorization: Basic "
1025                    + "dXNlcm5hbWU6cGFzc3dvcmQ="); // "dXNl..." == base64("username:password")
1026            assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
1027        }
1028    }
1029
1030    public void testAuthenticateWithGet() throws Exception {
1031        MockResponse pleaseAuthenticate = new MockResponse()
1032                .setResponseCode(401)
1033                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
1034                .setBody("Please authenticate.");
1035        // fail auth three times...
1036        server.enqueue(pleaseAuthenticate);
1037        server.enqueue(pleaseAuthenticate);
1038        server.enqueue(pleaseAuthenticate);
1039        // ...then succeed the fourth time
1040        server.enqueue(new MockResponse().setBody("Successful auth!"));
1041        server.play();
1042
1043        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
1044        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
1045        assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1046
1047        // no authorization header for the first request...
1048        RecordedRequest request = server.takeRequest();
1049        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
1050
1051        // ...but the three requests that follow requests include an authorization header
1052        for (int i = 0; i < 3; i++) {
1053            request = server.takeRequest();
1054            assertEquals("GET / HTTP/1.1", request.getRequestLine());
1055            assertContains(request.getHeaders(), "Authorization: Basic "
1056                    + "dXNlcm5hbWU6cGFzc3dvcmQ="); // "dXNl..." == base64("username:password")
1057        }
1058    }
1059
1060    public void testRedirectedWithChunkedEncoding() throws Exception {
1061        testRedirected(TransferKind.CHUNKED, true);
1062    }
1063
1064    public void testRedirectedWithContentLengthHeader() throws Exception {
1065        testRedirected(TransferKind.FIXED_LENGTH, true);
1066    }
1067
1068    public void testRedirectedWithNoLengthHeaders() throws Exception {
1069        testRedirected(TransferKind.END_OF_STREAM, false);
1070    }
1071
1072    private void testRedirected(TransferKind transferKind, boolean reuse) throws Exception {
1073        MockResponse response = new MockResponse()
1074                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1075                .addHeader("Location: /foo");
1076        transferKind.setBody(response, "This page has moved!", 10);
1077        server.enqueue(response);
1078        server.enqueue(new MockResponse().setBody("This is the new location!"));
1079        server.play();
1080
1081        URLConnection connection = server.getUrl("/").openConnection();
1082        assertEquals("This is the new location!",
1083                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1084
1085        RecordedRequest first = server.takeRequest();
1086        assertEquals("GET / HTTP/1.1", first.getRequestLine());
1087        RecordedRequest retry = server.takeRequest();
1088        assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
1089        if (reuse) {
1090            assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
1091        }
1092    }
1093
1094    public void testRedirectedOnHttps() throws IOException, InterruptedException {
1095        TestSSLContext testSSLContext = TestSSLContext.create();
1096        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
1097        server.enqueue(new MockResponse()
1098                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1099                .addHeader("Location: /foo")
1100                .setBody("This page has moved!"));
1101        server.enqueue(new MockResponse().setBody("This is the new location!"));
1102        server.play();
1103
1104        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
1105        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
1106        assertEquals("This is the new location!",
1107                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1108
1109        RecordedRequest first = server.takeRequest();
1110        assertEquals("GET / HTTP/1.1", first.getRequestLine());
1111        RecordedRequest retry = server.takeRequest();
1112        assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
1113        assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
1114    }
1115
1116    public void testNotRedirectedFromHttpsToHttp() throws IOException, InterruptedException {
1117        TestSSLContext testSSLContext = TestSSLContext.create();
1118        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
1119        server.enqueue(new MockResponse()
1120                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1121                .addHeader("Location: http://anyhost/foo")
1122                .setBody("This page has moved!"));
1123        server.play();
1124
1125        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
1126        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
1127        assertEquals("This page has moved!",
1128                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1129    }
1130
1131    public void testNotRedirectedFromHttpToHttps() throws IOException, InterruptedException {
1132        server.enqueue(new MockResponse()
1133                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1134                .addHeader("Location: https://anyhost/foo")
1135                .setBody("This page has moved!"));
1136        server.play();
1137
1138        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
1139        assertEquals("This page has moved!",
1140                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1141    }
1142
1143    public void testRedirectToAnotherOriginServer() throws Exception {
1144        MockWebServer server2 = new MockWebServer();
1145        server2.enqueue(new MockResponse().setBody("This is the 2nd server!"));
1146        server2.play();
1147
1148        server.enqueue(new MockResponse()
1149                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1150                .addHeader("Location: " + server2.getUrl("/").toString())
1151                .setBody("This page has moved!"));
1152        server.enqueue(new MockResponse().setBody("This is the first server again!"));
1153        server.play();
1154
1155        URLConnection connection = server.getUrl("/").openConnection();
1156        assertEquals("This is the 2nd server!",
1157                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1158        assertEquals(server2.getUrl("/"), connection.getURL());
1159
1160        // make sure the first server was careful to recycle the connection
1161        assertEquals("This is the first server again!",
1162                readAscii(server.getUrl("/").openStream(), Integer.MAX_VALUE));
1163
1164        RecordedRequest first = server.takeRequest();
1165        assertContains(first.getHeaders(), "Host: " + hostname + ":" + server.getPort());
1166        RecordedRequest second = server2.takeRequest();
1167        assertContains(second.getHeaders(), "Host: " + hostname + ":" + server2.getPort());
1168        RecordedRequest third = server.takeRequest();
1169        assertEquals("Expected connection reuse", 1, third.getSequenceNumber());
1170
1171        server2.shutdown();
1172    }
1173
1174    public void testHttpsWithCustomTrustManager() throws Exception {
1175        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
1176        RecordingTrustManager trustManager = new RecordingTrustManager();
1177        SSLContext sc = SSLContext.getInstance("TLS");
1178        sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());
1179
1180        HostnameVerifier defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
1181        HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
1182        SSLSocketFactory defaultSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
1183        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
1184        try {
1185            TestSSLContext testSSLContext = TestSSLContext.create();
1186            server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
1187            server.enqueue(new MockResponse().setBody("ABC"));
1188            server.enqueue(new MockResponse().setBody("DEF"));
1189            server.enqueue(new MockResponse().setBody("GHI"));
1190            server.play();
1191
1192            URL url = server.getUrl("/");
1193            assertEquals("ABC", readAscii(url.openStream(), Integer.MAX_VALUE));
1194            assertEquals("DEF", readAscii(url.openStream(), Integer.MAX_VALUE));
1195            assertEquals("GHI", readAscii(url.openStream(), Integer.MAX_VALUE));
1196
1197            assertEquals(Arrays.asList("verify " + hostname), hostnameVerifier.calls);
1198            assertEquals(Arrays.asList("checkServerTrusted ["
1199                                       + "CN=" + hostname + " 1, "
1200                                       + "CN=Test Intermediate Certificate Authority 1, "
1201                                       + "CN=Test Root Certificate Authority 1"
1202                                       + "] RSA"),
1203                    trustManager.calls);
1204        } finally {
1205            HttpsURLConnection.setDefaultHostnameVerifier(defaultHostnameVerifier);
1206            HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory);
1207        }
1208    }
1209
1210    public void testConnectTimeouts() throws IOException {
1211        // 10.0.0.0 is non-routable and will time out on every network
1212        URLConnection urlConnection = new URL("http://10.0.0.0/").openConnection();
1213        urlConnection.setConnectTimeout(1000);
1214        try {
1215            urlConnection.getInputStream();
1216            fail();
1217        } catch (SocketTimeoutException expected) {
1218        }
1219    }
1220
1221    public void testReadTimeouts() throws IOException {
1222        /*
1223         * This relies on the fact that MockWebServer doesn't close the
1224         * connection after a response has been sent. This causes the client to
1225         * try to read more bytes than are sent, which results in a timeout.
1226         */
1227        MockResponse timeout = new MockResponse()
1228                .setBody("ABC")
1229                .clearHeaders()
1230                .addHeader("Content-Length: 4");
1231        server.enqueue(timeout);
1232        server.play();
1233
1234        URLConnection urlConnection = server.getUrl("/").openConnection();
1235        urlConnection.setReadTimeout(1000);
1236        InputStream in = urlConnection.getInputStream();
1237        assertEquals('A', in.read());
1238        assertEquals('B', in.read());
1239        assertEquals('C', in.read());
1240        try {
1241            in.read(); // if Content-Length was accurate, this would return -1 immediately
1242            fail();
1243        } catch (SocketTimeoutException expected) {
1244        }
1245    }
1246
1247    public void testSetChunkedEncodingAsRequestProperty() throws IOException, InterruptedException {
1248        server.enqueue(new MockResponse());
1249        server.play();
1250
1251        HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
1252        urlConnection.setRequestProperty("Transfer-encoding", "chunked");
1253        urlConnection.setDoOutput(true);
1254        urlConnection.getOutputStream().write("ABC".getBytes("UTF-8"));
1255        assertEquals(200, urlConnection.getResponseCode());
1256
1257        RecordedRequest request = server.takeRequest();
1258        assertEquals("ABC", new String(request.getBody(), "UTF-8"));
1259    }
1260
1261    public void testConnectionCloseInRequest() throws IOException, InterruptedException {
1262        server.enqueue(new MockResponse()); // server doesn't honor the connection: close header!
1263        server.enqueue(new MockResponse());
1264        server.play();
1265
1266        HttpURLConnection a = (HttpURLConnection) server.getUrl("/").openConnection();
1267        a.setRequestProperty("Connection", "close");
1268        assertEquals(200, a.getResponseCode());
1269
1270        HttpURLConnection b = (HttpURLConnection) server.getUrl("/").openConnection();
1271        assertEquals(200, b.getResponseCode());
1272
1273        assertEquals(0, server.takeRequest().getSequenceNumber());
1274        assertEquals("When connection: close is used, each request should get its own connection",
1275                0, server.takeRequest().getSequenceNumber());
1276    }
1277
1278    public void testConnectionCloseInResponse() throws IOException, InterruptedException {
1279        server.enqueue(new MockResponse().addHeader("Connection: close"));
1280        server.enqueue(new MockResponse());
1281        server.play();
1282
1283        HttpURLConnection a = (HttpURLConnection) server.getUrl("/").openConnection();
1284        assertEquals(200, a.getResponseCode());
1285
1286        HttpURLConnection b = (HttpURLConnection) server.getUrl("/").openConnection();
1287        assertEquals(200, b.getResponseCode());
1288
1289        assertEquals(0, server.takeRequest().getSequenceNumber());
1290        assertEquals("When connection: close is used, each request should get its own connection",
1291                0, server.takeRequest().getSequenceNumber());
1292    }
1293
1294    public void testConnectionCloseWithRedirect() throws IOException, InterruptedException {
1295        MockResponse response = new MockResponse()
1296                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1297                .addHeader("Location: /foo")
1298                .addHeader("Connection: close");
1299        server.enqueue(response);
1300        server.enqueue(new MockResponse().setBody("This is the new location!"));
1301        server.play();
1302
1303        URLConnection connection = server.getUrl("/").openConnection();
1304        assertEquals("This is the new location!",
1305                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1306
1307        assertEquals(0, server.takeRequest().getSequenceNumber());
1308        assertEquals("When connection: close is used, each request should get its own connection",
1309                0, server.takeRequest().getSequenceNumber());
1310    }
1311
1312    public void testResponseCodeDisagreesWithHeaders() throws IOException, InterruptedException {
1313        server.enqueue(new MockResponse()
1314                .setResponseCode(HttpURLConnection.HTTP_NO_CONTENT)
1315                .setBody("This body is not allowed!"));
1316        server.play();
1317
1318        URLConnection connection = server.getUrl("/").openConnection();
1319        assertEquals("This body is not allowed!",
1320                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1321    }
1322
1323    public void testSingleByteReadIsSigned() throws IOException {
1324        server.enqueue(new MockResponse().setBody(new byte[] { -2, -1 }));
1325        server.play();
1326
1327        URLConnection connection = server.getUrl("/").openConnection();
1328        InputStream in = connection.getInputStream();
1329        assertEquals(254, in.read());
1330        assertEquals(255, in.read());
1331        assertEquals(-1, in.read());
1332    }
1333
1334    /**
1335     * Encodes the response body using GZIP and adds the corresponding header.
1336     */
1337    public byte[] gzip(byte[] bytes) throws IOException {
1338        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
1339        OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
1340        gzippedOut.write(bytes);
1341        gzippedOut.close();
1342        return bytesOut.toByteArray();
1343    }
1344
1345    /**
1346     * Reads at most {@code limit} characters from {@code in} and asserts that
1347     * content equals {@code expected}.
1348     */
1349    private void assertContent(String expected, URLConnection connection, int limit)
1350            throws IOException {
1351        assertEquals(expected, readAscii(connection.getInputStream(), limit));
1352        ((HttpURLConnection) connection).disconnect();
1353    }
1354
1355    private void assertContent(String expected, URLConnection connection) throws IOException {
1356        assertContent(expected, connection, Integer.MAX_VALUE);
1357    }
1358
1359    private void assertContains(List<String> headers, String header) {
1360        assertTrue(headers.toString(), headers.contains(header));
1361    }
1362
1363    private void assertContainsNoneMatching(List<String> headers, String pattern) {
1364        for (String header : headers) {
1365            if (header.matches(pattern)) {
1366                fail("Header " + header + " matches " + pattern);
1367            }
1368        }
1369    }
1370
1371    private Set<String> newSet(String... elements) {
1372        return new HashSet<String>(Arrays.asList(elements));
1373    }
1374
1375    enum TransferKind {
1376        CHUNKED() {
1377            @Override void setBody(MockResponse response, byte[] content, int chunkSize)
1378                    throws IOException {
1379                response.setChunkedBody(content, chunkSize);
1380            }
1381        },
1382        FIXED_LENGTH() {
1383            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
1384                response.setBody(content);
1385            }
1386        },
1387        END_OF_STREAM() {
1388            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
1389                response.setBody(content);
1390                response.setDisconnectAtEnd(true);
1391                for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
1392                    if (h.next().startsWith("Content-Length:")) {
1393                        h.remove();
1394                        break;
1395                    }
1396                }
1397            }
1398        };
1399
1400        abstract void setBody(MockResponse response, byte[] content, int chunkSize)
1401                throws IOException;
1402
1403        void setBody(MockResponse response, String content, int chunkSize) throws IOException {
1404            setBody(response, content.getBytes("UTF-8"), chunkSize);
1405        }
1406    }
1407
1408    private static class RecordingTrustManager implements X509TrustManager {
1409        private final List<String> calls = new ArrayList<String>();
1410
1411        public X509Certificate[] getAcceptedIssuers() {
1412            calls.add("getAcceptedIssuers");
1413            return new X509Certificate[] {};
1414        }
1415
1416        public void checkClientTrusted(X509Certificate[] chain, String authType)
1417                throws CertificateException {
1418            calls.add("checkClientTrusted " + certificatesToString(chain) + " " + authType);
1419        }
1420
1421        public void checkServerTrusted(X509Certificate[] chain, String authType)
1422                throws CertificateException {
1423            calls.add("checkServerTrusted " + certificatesToString(chain) + " " + authType);
1424        }
1425
1426        private String certificatesToString(X509Certificate[] certificates) {
1427            List<String> result = new ArrayList<String>();
1428            for (X509Certificate certificate : certificates) {
1429                result.add(certificate.getSubjectDN() + " " + certificate.getSerialNumber());
1430            }
1431            return result.toString();
1432        }
1433    }
1434
1435    private static class RecordingHostnameVerifier implements HostnameVerifier {
1436        private final List<String> calls = new ArrayList<String>();
1437
1438        public boolean verify(String hostname, SSLSession session) {
1439            calls.add("verify " + hostname);
1440            return true;
1441        }
1442    }
1443}
1444