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