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