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