URLConnectionTest.java revision 4557728efb66c455a52b7669a8eefef7a9e54854
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 libcore.javax.net.ssl.TestSSLContext;
57import javax.net.ssl.TrustManager;
58import javax.net.ssl.X509TrustManager;
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    public void testClientConfiguredGzipContentEncoding() throws Exception {
768        server.enqueue(new MockResponse()
769                .setBody(gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8")))
770                .addHeader("Content-Encoding: gzip"));
771        server.play();
772
773        URLConnection connection = server.getUrl("/").openConnection();
774        connection.addRequestProperty("Accept-Encoding", "gzip");
775        InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
776        assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", readAscii(gunzippedIn, Integer.MAX_VALUE));
777
778        RecordedRequest request = server.takeRequest();
779        assertContains(request.getHeaders(), "Accept-Encoding: gzip");
780    }
781
782    public void testGzipAndConnectionReuseWithFixedLength() throws Exception {
783        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH);
784    }
785
786    public void testGzipAndConnectionReuseWithChunkedEncoding() throws Exception {
787        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED);
788    }
789
790    /**
791     * Test a bug where gzip input streams weren't exhausting the input stream,
792     * which corrupted the request that followed.
793     * http://code.google.com/p/android/issues/detail?id=7059
794     */
795    private void testClientConfiguredGzipContentEncodingAndConnectionReuse(
796            TransferKind transferKind) throws Exception {
797        MockResponse responseOne = new MockResponse();
798        responseOne.addHeader("Content-Encoding: gzip");
799        transferKind.setBody(responseOne, gzip("one (gzipped)".getBytes("UTF-8")), 5);
800        server.enqueue(responseOne);
801        MockResponse responseTwo = new MockResponse();
802        transferKind.setBody(responseTwo, "two (identity)", 5);
803        server.enqueue(responseTwo);
804        server.play();
805
806        URLConnection connection = server.getUrl("/").openConnection();
807        connection.addRequestProperty("Accept-Encoding", "gzip");
808        InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
809        assertEquals("one (gzipped)", readAscii(gunzippedIn, Integer.MAX_VALUE));
810        assertEquals(0, server.takeRequest().getSequenceNumber());
811
812        connection = server.getUrl("/").openConnection();
813        assertEquals("two (identity)", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
814        assertEquals(1, server.takeRequest().getSequenceNumber());
815    }
816
817    /**
818     * Obnoxiously test that the chunk sizes transmitted exactly equal the
819     * requested data+chunk header size. Although setChunkedStreamingMode()
820     * isn't specific about whether the size applies to the data or the
821     * complete chunk, the RI interprets it as a complete chunk.
822     */
823    public void testSetChunkedStreamingMode() throws IOException, InterruptedException {
824        server.enqueue(new MockResponse());
825        server.play();
826
827        HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
828        urlConnection.setChunkedStreamingMode(8);
829        urlConnection.setDoOutput(true);
830        OutputStream outputStream = urlConnection.getOutputStream();
831        outputStream.write("ABCDEFGHIJKLMNOPQ".getBytes("US-ASCII"));
832        assertEquals(200, urlConnection.getResponseCode());
833
834        RecordedRequest request = server.takeRequest();
835        assertEquals("ABCDEFGHIJKLMNOPQ", new String(request.getBody(), "US-ASCII"));
836        assertEquals(Arrays.asList(3, 3, 3, 3, 3, 2), request.getChunkSizes());
837    }
838
839    public void testAuthenticateWithFixedLengthStreaming() throws Exception {
840        testAuthenticateWithStreamingPost(StreamingMode.FIXED_LENGTH);
841    }
842
843    public void testAuthenticateWithChunkedStreaming() throws Exception {
844        testAuthenticateWithStreamingPost(StreamingMode.CHUNKED);
845    }
846
847    private void testAuthenticateWithStreamingPost(StreamingMode streamingMode) throws Exception {
848        MockResponse pleaseAuthenticate = new MockResponse()
849                .setResponseCode(401)
850                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
851                .setBody("Please authenticate.");
852        server.enqueue(pleaseAuthenticate);
853        server.play();
854
855        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
856        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
857        connection.setDoOutput(true);
858        byte[] requestBody = { 'A', 'B', 'C', 'D' };
859        if (streamingMode == StreamingMode.FIXED_LENGTH) {
860            connection.setFixedLengthStreamingMode(requestBody.length);
861        } else if (streamingMode == StreamingMode.CHUNKED) {
862            connection.setChunkedStreamingMode(0);
863        }
864        OutputStream outputStream = connection.getOutputStream();
865        outputStream.write(requestBody);
866        outputStream.close();
867        try {
868            connection.getInputStream();
869            fail();
870        } catch (HttpRetryException expected) {
871        }
872
873        // no authorization header for the request...
874        RecordedRequest request = server.takeRequest();
875        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
876        assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
877    }
878
879    enum StreamingMode {
880        FIXED_LENGTH, CHUNKED
881    }
882
883    public void testAuthenticateWithPost() throws Exception {
884        MockResponse pleaseAuthenticate = new MockResponse()
885                .setResponseCode(401)
886                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
887                .setBody("Please authenticate.");
888        // fail auth three times...
889        server.enqueue(pleaseAuthenticate);
890        server.enqueue(pleaseAuthenticate);
891        server.enqueue(pleaseAuthenticate);
892        // ...then succeed the fourth time
893        server.enqueue(new MockResponse().setBody("Successful auth!"));
894        server.play();
895
896        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
897        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
898        connection.setDoOutput(true);
899        byte[] requestBody = { 'A', 'B', 'C', 'D' };
900        OutputStream outputStream = connection.getOutputStream();
901        outputStream.write(requestBody);
902        outputStream.close();
903        assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
904
905        // no authorization header for the first request...
906        RecordedRequest request = server.takeRequest();
907        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
908
909        // ...but the three requests that follow include an authorization header
910        for (int i = 0; i < 3; i++) {
911            request = server.takeRequest();
912            assertEquals("POST / HTTP/1.1", request.getRequestLine());
913            assertContains(request.getHeaders(), "Authorization: Basic "
914                    + "dXNlcm5hbWU6cGFzc3dvcmQ="); // "dXNl..." == base64("username:password")
915            assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
916        }
917    }
918
919    public void testAuthenticateWithGet() throws Exception {
920        MockResponse pleaseAuthenticate = new MockResponse()
921                .setResponseCode(401)
922                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
923                .setBody("Please authenticate.");
924        // fail auth three times...
925        server.enqueue(pleaseAuthenticate);
926        server.enqueue(pleaseAuthenticate);
927        server.enqueue(pleaseAuthenticate);
928        // ...then succeed the fourth time
929        server.enqueue(new MockResponse().setBody("Successful auth!"));
930        server.play();
931
932        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
933        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
934        assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
935
936        // no authorization header for the first request...
937        RecordedRequest request = server.takeRequest();
938        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
939
940        // ...but the three requests that follow requests include an authorization header
941        for (int i = 0; i < 3; i++) {
942            request = server.takeRequest();
943            assertEquals("GET / HTTP/1.1", request.getRequestLine());
944            assertContains(request.getHeaders(), "Authorization: Basic "
945                    + "dXNlcm5hbWU6cGFzc3dvcmQ="); // "dXNl..." == base64("username:password")
946        }
947    }
948
949    public void testRedirectedWithChunkedEncoding() throws Exception {
950        testRedirected(TransferKind.CHUNKED, true);
951    }
952
953    public void testRedirectedWithContentLengthHeader() throws Exception {
954        testRedirected(TransferKind.FIXED_LENGTH, true);
955    }
956
957    public void testRedirectedWithNoLengthHeaders() throws Exception {
958        testRedirected(TransferKind.END_OF_STREAM, false);
959    }
960
961    private void testRedirected(TransferKind transferKind, boolean reuse) throws Exception {
962        MockResponse response = new MockResponse()
963                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
964                .addHeader("Location: /foo");
965        transferKind.setBody(response, "This page has moved!", 10);
966        server.enqueue(response);
967        server.enqueue(new MockResponse().setBody("This is the new location!"));
968        server.play();
969
970        URLConnection connection = server.getUrl("/").openConnection();
971        assertEquals("This is the new location!",
972                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
973
974        RecordedRequest first = server.takeRequest();
975        assertEquals("GET / HTTP/1.1", first.getRequestLine());
976        RecordedRequest retry = server.takeRequest();
977        assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
978        if (reuse) {
979            assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
980        }
981    }
982
983    public void testRedirectedOnHttps() throws IOException, InterruptedException {
984        TestSSLContext testSSLContext = TestSSLContext.create();
985        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
986        server.enqueue(new MockResponse()
987                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
988                .addHeader("Location: /foo")
989                .setBody("This page has moved!"));
990        server.enqueue(new MockResponse().setBody("This is the new location!"));
991        server.play();
992
993        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
994        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
995        assertEquals("This is the new location!",
996                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
997
998        RecordedRequest first = server.takeRequest();
999        assertEquals("GET / HTTP/1.1", first.getRequestLine());
1000        RecordedRequest retry = server.takeRequest();
1001        assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
1002        assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
1003    }
1004
1005    public void testNotRedirectedFromHttpsToHttp() throws IOException, InterruptedException {
1006        TestSSLContext testSSLContext = TestSSLContext.create();
1007        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
1008        server.enqueue(new MockResponse()
1009                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1010                .addHeader("Location: http://anyhost/foo")
1011                .setBody("This page has moved!"));
1012        server.play();
1013
1014        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
1015        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
1016        assertEquals("This page has moved!",
1017                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1018    }
1019
1020    public void testNotRedirectedFromHttpToHttps() throws IOException, InterruptedException {
1021        server.enqueue(new MockResponse()
1022                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1023                .addHeader("Location: https://anyhost/foo")
1024                .setBody("This page has moved!"));
1025        server.play();
1026
1027        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
1028        assertEquals("This page has moved!",
1029                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1030    }
1031
1032    public void testRedirectToAnotherOriginServer() throws Exception {
1033        MockWebServer server2 = new MockWebServer();
1034        server2.enqueue(new MockResponse().setBody("This is the 2nd server!"));
1035        server2.play();
1036
1037        server.enqueue(new MockResponse()
1038                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1039                .addHeader("Location: " + server2.getUrl("/").toString())
1040                .setBody("This page has moved!"));
1041        server.enqueue(new MockResponse().setBody("This is the first server again!"));
1042        server.play();
1043
1044        URLConnection connection = server.getUrl("/").openConnection();
1045        assertEquals("This is the 2nd server!",
1046                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1047        assertEquals(server2.getUrl("/"), connection.getURL());
1048
1049        // make sure the first server was careful to recycle the connection
1050        assertEquals("This is the first server again!",
1051                readAscii(server.getUrl("/").openStream(), Integer.MAX_VALUE));
1052
1053        RecordedRequest first = server.takeRequest();
1054        assertContains(first.getHeaders(), "Host: localhost:" + server.getPort());
1055        RecordedRequest second = server2.takeRequest();
1056        assertContains(second.getHeaders(), "Host: localhost:" + server2.getPort());
1057        RecordedRequest third = server.takeRequest();
1058        assertEquals("Expected connection reuse", 1, third.getSequenceNumber());
1059
1060        server2.shutdown();
1061    }
1062
1063    public void testHttpsWithCustomTrustManager() throws Exception {
1064        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
1065        RecordingTrustManager trustManager = new RecordingTrustManager();
1066        SSLContext sc = SSLContext.getInstance("TLS");
1067        sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());
1068
1069        HostnameVerifier defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
1070        HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
1071        SSLSocketFactory defaultSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
1072        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
1073        try {
1074            TestSSLContext testSSLContext = TestSSLContext.create();
1075            server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
1076            server.enqueue(new MockResponse().setBody("ABC"));
1077            server.enqueue(new MockResponse().setBody("DEF"));
1078            server.enqueue(new MockResponse().setBody("GHI"));
1079            server.play();
1080
1081            URL url = server.getUrl("/");
1082            assertEquals("ABC", readAscii(url.openStream(), Integer.MAX_VALUE));
1083            assertEquals("DEF", readAscii(url.openStream(), Integer.MAX_VALUE));
1084            assertEquals("GHI", readAscii(url.openStream(), Integer.MAX_VALUE));
1085
1086            assertEquals(Arrays.asList("verify localhost"), hostnameVerifier.calls);
1087            assertEquals(Arrays.asList("checkServerTrusted ["
1088                                       + "CN=localhost 1, "
1089                                       + "CN=Test Intermediate Certificate Authority 1, "
1090                                       + "CN=Test Root Certificate Authority 1"
1091                                       + "] RSA"),
1092                    trustManager.calls);
1093        } finally {
1094            HttpsURLConnection.setDefaultHostnameVerifier(defaultHostnameVerifier);
1095            HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory);
1096        }
1097    }
1098
1099    public void testConnectTimeouts() throws IOException {
1100        // 10.0.0.0 is non-routable and will time out on every network
1101        URLConnection urlConnection = new URL("http://10.0.0.0/").openConnection();
1102        urlConnection.setConnectTimeout(1000);
1103        try {
1104            urlConnection.getInputStream();
1105            fail();
1106        } catch (SocketTimeoutException expected) {
1107        }
1108    }
1109
1110    public void testReadTimeouts() throws IOException {
1111        /*
1112         * This relies on the fact that MockWebServer doesn't close the
1113         * connection after a response has been sent. This causes the client to
1114         * try to read more bytes than are sent, which results in a timeout.
1115         */
1116        MockResponse timeout = new MockResponse()
1117                .setBody("ABC")
1118                .clearHeaders()
1119                .addHeader("Content-Length: 4");
1120        server.enqueue(timeout);
1121        server.play();
1122
1123        URLConnection urlConnection = server.getUrl("/").openConnection();
1124        urlConnection.setReadTimeout(1000);
1125        InputStream in = urlConnection.getInputStream();
1126        assertEquals('A', in.read());
1127        assertEquals('B', in.read());
1128        assertEquals('C', in.read());
1129        try {
1130            in.read(); // if Content-Length was accurate, this would return -1 immediately
1131            fail();
1132        } catch (SocketTimeoutException expected) {
1133        }
1134    }
1135
1136    /**
1137     * Encodes the response body using GZIP and adds the corresponding header.
1138     */
1139    public byte[] gzip(byte[] bytes) throws IOException {
1140        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
1141        OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
1142        gzippedOut.write(bytes);
1143        gzippedOut.close();
1144        return bytesOut.toByteArray();
1145    }
1146
1147    /**
1148     * Reads at most {@code limit} characters from {@code in} and asserts that
1149     * content equals {@code expected}.
1150     */
1151    private void assertContent(String expected, URLConnection connection, int limit)
1152            throws IOException {
1153        assertEquals(expected, readAscii(connection.getInputStream(), limit));
1154        ((HttpURLConnection) connection).disconnect();
1155    }
1156
1157    private void assertContent(String expected, URLConnection connection) throws IOException {
1158        assertContent(expected, connection, Integer.MAX_VALUE);
1159    }
1160
1161    private void assertContains(List<String> headers, String header) {
1162        assertTrue(headers.toString(), headers.contains(header));
1163    }
1164
1165    private void assertContainsNoneMatching(List<String> headers, String pattern) {
1166        for (String header : headers) {
1167            if (header.matches(pattern)) {
1168                fail("Header " + header + " matches " + pattern);
1169            }
1170        }
1171    }
1172
1173    private Set<String> newSet(String... elements) {
1174        return new HashSet<String>(Arrays.asList(elements));
1175    }
1176
1177    enum TransferKind {
1178        CHUNKED() {
1179            @Override void setBody(MockResponse response, byte[] content, int chunkSize)
1180                    throws IOException {
1181                response.setChunkedBody(content, chunkSize);
1182            }
1183        },
1184        FIXED_LENGTH() {
1185            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
1186                response.setBody(content);
1187            }
1188        },
1189        END_OF_STREAM() {
1190            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
1191                response.setBody(content);
1192                response.setDisconnectAtEnd(true);
1193                for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
1194                    if (h.next().startsWith("Content-Length:")) {
1195                        h.remove();
1196                        break;
1197                    }
1198                }
1199            }
1200        };
1201
1202        abstract void setBody(MockResponse response, byte[] content, int chunkSize)
1203                throws IOException;
1204
1205        void setBody(MockResponse response, String content, int chunkSize) throws IOException {
1206            setBody(response, content.getBytes("UTF-8"), chunkSize);
1207        }
1208    }
1209
1210    private static class RecordingTrustManager implements X509TrustManager {
1211        private final List<String> calls = new ArrayList<String>();
1212
1213        public X509Certificate[] getAcceptedIssuers() {
1214            calls.add("getAcceptedIssuers");
1215            return new X509Certificate[] {};
1216        }
1217
1218        public void checkClientTrusted(X509Certificate[] chain, String authType)
1219                throws CertificateException {
1220            calls.add("checkClientTrusted " + certificatesToString(chain) + " " + authType);
1221        }
1222
1223        public void checkServerTrusted(X509Certificate[] chain, String authType)
1224                throws CertificateException {
1225            calls.add("checkServerTrusted " + certificatesToString(chain) + " " + authType);
1226        }
1227
1228        private String certificatesToString(X509Certificate[] certificates) {
1229            List<String> result = new ArrayList<String>();
1230            for (X509Certificate certificate : certificates) {
1231                result.add(certificate.getSubjectDN() + " " + certificate.getSerialNumber());
1232            }
1233            return result.toString();
1234        }
1235    }
1236
1237    private static class RecordingHostnameVerifier implements HostnameVerifier {
1238        private final List<String> calls = new ArrayList<String>();
1239
1240        public boolean verify(String hostname, SSLSession session) {
1241            calls.add("verify " + hostname);
1242            return true;
1243        }
1244    }
1245}
1246