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