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