URLConnectionTest.java revision 0c59055dd24e1659f85d9ff7e2148883f663bd62
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.play();
651
652        InputStream in = server.getUrl("/").openConnection().getInputStream();
653        assertFalse("This implementation claims to support mark().", in.markSupported());
654        in.mark(5);
655        assertEquals("ABCDE", readAscii(in, 5));
656        try {
657            in.reset();
658            fail();
659        } catch (IOException expected) {
660        }
661        assertEquals("FGHIJKLMNOPQRSTUVWXYZ", readAscii(in, Integer.MAX_VALUE));
662        assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", server.getUrl("/").openConnection());
663    }
664
665    /**
666     * We've had a bug where we forget the HTTP response when we see response
667     * code 401. This causes a new HTTP request to be issued for every call into
668     * the URLConnection.
669     */
670    public void testUnauthorizedResponseHandling() throws IOException {
671        MockResponse response = new MockResponse()
672                .addHeader("WWW-Authenticate: challenge")
673                .setResponseCode(401) // UNAUTHORIZED
674                .setBody("Unauthorized");
675        server.enqueue(response);
676        server.enqueue(response);
677        server.enqueue(response);
678        server.play();
679
680        URL url = server.getUrl("/");
681        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
682
683        assertEquals(401, conn.getResponseCode());
684        assertEquals(401, conn.getResponseCode());
685        assertEquals(401, conn.getResponseCode());
686        assertEquals(1, server.getRequestCount());
687    }
688
689    public void testNonHexChunkSize() throws IOException {
690        server.enqueue(new MockResponse()
691                .setBody("5\r\nABCDE\r\nG\r\nFGHIJKLMNOPQRSTU\r\n0\r\n\r\n")
692                .clearHeaders()
693                .addHeader("Transfer-encoding: chunked"));
694        server.play();
695
696        URLConnection connection = server.getUrl("/").openConnection();
697        try {
698            readAscii(connection.getInputStream(), Integer.MAX_VALUE);
699            fail();
700        } catch (IOException e) {
701        }
702    }
703
704    public void testMissingChunkBody() throws IOException {
705        server.enqueue(new MockResponse()
706                .setBody("5")
707                .clearHeaders()
708                .addHeader("Transfer-encoding: chunked")
709                .setSocketPolicy(DISCONNECT_AT_END));
710        server.play();
711
712        URLConnection connection = server.getUrl("/").openConnection();
713        try {
714            readAscii(connection.getInputStream(), Integer.MAX_VALUE);
715            fail();
716        } catch (IOException e) {
717        }
718    }
719
720    /**
721     * This test checks whether connections are gzipped by default. This
722     * behavior in not required by the API, so a failure of this test does not
723     * imply a bug in the implementation.
724     */
725    public void testGzipEncodingEnabledByDefault() throws IOException, InterruptedException {
726        server.enqueue(new MockResponse()
727                .setBody(gzip("ABCABCABC".getBytes("UTF-8")))
728                .addHeader("Content-Encoding: gzip"));
729        server.play();
730
731        URLConnection connection = server.getUrl("/").openConnection();
732        assertEquals("ABCABCABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
733        assertNull(connection.getContentEncoding());
734
735        RecordedRequest request = server.takeRequest();
736        assertContains(request.getHeaders(), "Accept-Encoding: gzip");
737    }
738
739    public void testClientConfiguredGzipContentEncoding() throws Exception {
740        server.enqueue(new MockResponse()
741                .setBody(gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8")))
742                .addHeader("Content-Encoding: gzip"));
743        server.play();
744
745        URLConnection connection = server.getUrl("/").openConnection();
746        connection.addRequestProperty("Accept-Encoding", "gzip");
747        InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
748        assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", readAscii(gunzippedIn, Integer.MAX_VALUE));
749
750        RecordedRequest request = server.takeRequest();
751        assertContains(request.getHeaders(), "Accept-Encoding: gzip");
752    }
753
754    public void testGzipAndConnectionReuseWithFixedLength() throws Exception {
755        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH);
756    }
757
758    public void testGzipAndConnectionReuseWithChunkedEncoding() throws Exception {
759        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED);
760    }
761
762    public void testClientConfiguredCustomContentEncoding() throws Exception {
763        server.enqueue(new MockResponse()
764                .setBody("ABCDE")
765                .addHeader("Content-Encoding: custom"));
766        server.play();
767
768        URLConnection connection = server.getUrl("/").openConnection();
769        connection.addRequestProperty("Accept-Encoding", "custom");
770        assertEquals("ABCDE", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
771
772        RecordedRequest request = server.takeRequest();
773        assertContains(request.getHeaders(), "Accept-Encoding: custom");
774    }
775
776    /**
777     * Test a bug where gzip input streams weren't exhausting the input stream,
778     * which corrupted the request that followed.
779     * http://code.google.com/p/android/issues/detail?id=7059
780     */
781    private void testClientConfiguredGzipContentEncodingAndConnectionReuse(
782            TransferKind transferKind) throws Exception {
783        MockResponse responseOne = new MockResponse();
784        responseOne.addHeader("Content-Encoding: gzip");
785        transferKind.setBody(responseOne, gzip("one (gzipped)".getBytes("UTF-8")), 5);
786        server.enqueue(responseOne);
787        MockResponse responseTwo = new MockResponse();
788        transferKind.setBody(responseTwo, "two (identity)", 5);
789        server.enqueue(responseTwo);
790        server.play();
791
792        URLConnection connection = server.getUrl("/").openConnection();
793        connection.addRequestProperty("Accept-Encoding", "gzip");
794        InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
795        assertEquals("one (gzipped)", readAscii(gunzippedIn, Integer.MAX_VALUE));
796        assertEquals(0, server.takeRequest().getSequenceNumber());
797
798        connection = server.getUrl("/").openConnection();
799        assertEquals("two (identity)", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
800        assertEquals(1, server.takeRequest().getSequenceNumber());
801    }
802
803    /**
804     * Obnoxiously test that the chunk sizes transmitted exactly equal the
805     * requested data+chunk header size. Although setChunkedStreamingMode()
806     * isn't specific about whether the size applies to the data or the
807     * complete chunk, the RI interprets it as a complete chunk.
808     */
809    public void testSetChunkedStreamingMode() throws IOException, InterruptedException {
810        server.enqueue(new MockResponse());
811        server.play();
812
813        HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
814        urlConnection.setChunkedStreamingMode(8);
815        urlConnection.setDoOutput(true);
816        OutputStream outputStream = urlConnection.getOutputStream();
817        outputStream.write("ABCDEFGHIJKLMNOPQ".getBytes("US-ASCII"));
818        assertEquals(200, urlConnection.getResponseCode());
819
820        RecordedRequest request = server.takeRequest();
821        assertEquals("ABCDEFGHIJKLMNOPQ", new String(request.getBody(), "US-ASCII"));
822        assertEquals(Arrays.asList(3, 3, 3, 3, 3, 2), request.getChunkSizes());
823    }
824
825    public void testAuthenticateWithFixedLengthStreaming() throws Exception {
826        testAuthenticateWithStreamingPost(StreamingMode.FIXED_LENGTH);
827    }
828
829    public void testAuthenticateWithChunkedStreaming() throws Exception {
830        testAuthenticateWithStreamingPost(StreamingMode.CHUNKED);
831    }
832
833    private void testAuthenticateWithStreamingPost(StreamingMode streamingMode) throws Exception {
834        MockResponse pleaseAuthenticate = new MockResponse()
835                .setResponseCode(401)
836                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
837                .setBody("Please authenticate.");
838        server.enqueue(pleaseAuthenticate);
839        server.play();
840
841        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
842        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
843        connection.setDoOutput(true);
844        byte[] requestBody = { 'A', 'B', 'C', 'D' };
845        if (streamingMode == StreamingMode.FIXED_LENGTH) {
846            connection.setFixedLengthStreamingMode(requestBody.length);
847        } else if (streamingMode == StreamingMode.CHUNKED) {
848            connection.setChunkedStreamingMode(0);
849        }
850        OutputStream outputStream = connection.getOutputStream();
851        outputStream.write(requestBody);
852        outputStream.close();
853        try {
854            connection.getInputStream();
855            fail();
856        } catch (HttpRetryException expected) {
857        }
858
859        // no authorization header for the request...
860        RecordedRequest request = server.takeRequest();
861        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
862        assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
863    }
864
865    public void testSecureFixedLengthStreaming() throws Exception {
866        testSecureStreamingPost(StreamingMode.FIXED_LENGTH);
867    }
868
869    public void testSecureChunkedStreaming() throws Exception {
870        testSecureStreamingPost(StreamingMode.CHUNKED);
871    }
872
873    /**
874     * Users have reported problems using HTTPS with streaming request bodies.
875     * http://code.google.com/p/android/issues/detail?id=12860
876     */
877    private void testSecureStreamingPost(StreamingMode streamingMode) throws Exception {
878        TestSSLContext testSSLContext = TestSSLContext.create();
879        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
880        server.enqueue(new MockResponse().setBody("Success!"));
881        server.play();
882
883        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
884        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
885        connection.setDoOutput(true);
886        byte[] requestBody = { 'A', 'B', 'C', 'D' };
887        if (streamingMode == StreamingMode.FIXED_LENGTH) {
888            connection.setFixedLengthStreamingMode(requestBody.length);
889        } else if (streamingMode == StreamingMode.CHUNKED) {
890            connection.setChunkedStreamingMode(0);
891        }
892        OutputStream outputStream = connection.getOutputStream();
893        outputStream.write(requestBody);
894        outputStream.close();
895        assertEquals("Success!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
896
897        RecordedRequest request = server.takeRequest();
898        assertEquals("POST / HTTP/1.1", request.getRequestLine());
899        if (streamingMode == StreamingMode.FIXED_LENGTH) {
900            assertEquals(Collections.<Integer>emptyList(), request.getChunkSizes());
901        } else if (streamingMode == StreamingMode.CHUNKED) {
902            assertEquals(Arrays.asList(4), request.getChunkSizes());
903        }
904        assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
905    }
906
907    enum StreamingMode {
908        FIXED_LENGTH, CHUNKED
909    }
910
911    public void testAuthenticateWithPost() throws Exception {
912        MockResponse pleaseAuthenticate = new MockResponse()
913                .setResponseCode(401)
914                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
915                .setBody("Please authenticate.");
916        // fail auth three times...
917        server.enqueue(pleaseAuthenticate);
918        server.enqueue(pleaseAuthenticate);
919        server.enqueue(pleaseAuthenticate);
920        // ...then succeed the fourth time
921        server.enqueue(new MockResponse().setBody("Successful auth!"));
922        server.play();
923
924        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
925        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
926        connection.setDoOutput(true);
927        byte[] requestBody = { 'A', 'B', 'C', 'D' };
928        OutputStream outputStream = connection.getOutputStream();
929        outputStream.write(requestBody);
930        outputStream.close();
931        assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
932
933        // no authorization header for the first request...
934        RecordedRequest request = server.takeRequest();
935        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
936
937        // ...but the three requests that follow include an authorization header
938        for (int i = 0; i < 3; i++) {
939            request = server.takeRequest();
940            assertEquals("POST / HTTP/1.1", request.getRequestLine());
941            assertContains(request.getHeaders(), "Authorization: Basic "
942                    + "dXNlcm5hbWU6cGFzc3dvcmQ="); // "dXNl..." == base64("username:password")
943            assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
944        }
945    }
946
947    public void testAuthenticateWithGet() throws Exception {
948        MockResponse pleaseAuthenticate = new MockResponse()
949                .setResponseCode(401)
950                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
951                .setBody("Please authenticate.");
952        // fail auth three times...
953        server.enqueue(pleaseAuthenticate);
954        server.enqueue(pleaseAuthenticate);
955        server.enqueue(pleaseAuthenticate);
956        // ...then succeed the fourth time
957        server.enqueue(new MockResponse().setBody("Successful auth!"));
958        server.play();
959
960        Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
961        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
962        assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
963
964        // no authorization header for the first request...
965        RecordedRequest request = server.takeRequest();
966        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
967
968        // ...but the three requests that follow requests include an authorization header
969        for (int i = 0; i < 3; i++) {
970            request = server.takeRequest();
971            assertEquals("GET / HTTP/1.1", request.getRequestLine());
972            assertContains(request.getHeaders(), "Authorization: Basic "
973                    + "dXNlcm5hbWU6cGFzc3dvcmQ="); // "dXNl..." == base64("username:password")
974        }
975    }
976
977    public void testRedirectedWithChunkedEncoding() throws Exception {
978        testRedirected(TransferKind.CHUNKED, true);
979    }
980
981    public void testRedirectedWithContentLengthHeader() throws Exception {
982        testRedirected(TransferKind.FIXED_LENGTH, true);
983    }
984
985    public void testRedirectedWithNoLengthHeaders() throws Exception {
986        testRedirected(TransferKind.END_OF_STREAM, false);
987    }
988
989    private void testRedirected(TransferKind transferKind, boolean reuse) throws Exception {
990        MockResponse response = new MockResponse()
991                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
992                .addHeader("Location: /foo");
993        transferKind.setBody(response, "This page has moved!", 10);
994        server.enqueue(response);
995        server.enqueue(new MockResponse().setBody("This is the new location!"));
996        server.play();
997
998        URLConnection connection = server.getUrl("/").openConnection();
999        assertEquals("This is the new location!",
1000                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1001
1002        RecordedRequest first = server.takeRequest();
1003        assertEquals("GET / HTTP/1.1", first.getRequestLine());
1004        RecordedRequest retry = server.takeRequest();
1005        assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
1006        if (reuse) {
1007            assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
1008        }
1009    }
1010
1011    public void testRedirectedOnHttps() throws IOException, InterruptedException {
1012        TestSSLContext testSSLContext = TestSSLContext.create();
1013        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
1014        server.enqueue(new MockResponse()
1015                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1016                .addHeader("Location: /foo")
1017                .setBody("This page has moved!"));
1018        server.enqueue(new MockResponse().setBody("This is the new location!"));
1019        server.play();
1020
1021        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
1022        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
1023        assertEquals("This is the new location!",
1024                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1025
1026        RecordedRequest first = server.takeRequest();
1027        assertEquals("GET / HTTP/1.1", first.getRequestLine());
1028        RecordedRequest retry = server.takeRequest();
1029        assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
1030        assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
1031    }
1032
1033    public void testNotRedirectedFromHttpsToHttp() throws IOException, InterruptedException {
1034        TestSSLContext testSSLContext = TestSSLContext.create();
1035        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
1036        server.enqueue(new MockResponse()
1037                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1038                .addHeader("Location: http://anyhost/foo")
1039                .setBody("This page has moved!"));
1040        server.play();
1041
1042        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
1043        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
1044        assertEquals("This page has moved!",
1045                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1046    }
1047
1048    public void testNotRedirectedFromHttpToHttps() throws IOException, InterruptedException {
1049        server.enqueue(new MockResponse()
1050                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1051                .addHeader("Location: https://anyhost/foo")
1052                .setBody("This page has moved!"));
1053        server.play();
1054
1055        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
1056        assertEquals("This page has moved!",
1057                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1058    }
1059
1060    public void testRedirectToAnotherOriginServer() throws Exception {
1061        MockWebServer server2 = new MockWebServer();
1062        server2.enqueue(new MockResponse().setBody("This is the 2nd server!"));
1063        server2.play();
1064
1065        server.enqueue(new MockResponse()
1066                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1067                .addHeader("Location: " + server2.getUrl("/").toString())
1068                .setBody("This page has moved!"));
1069        server.enqueue(new MockResponse().setBody("This is the first server again!"));
1070        server.play();
1071
1072        URLConnection connection = server.getUrl("/").openConnection();
1073        assertEquals("This is the 2nd server!",
1074                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1075        assertEquals(server2.getUrl("/"), connection.getURL());
1076
1077        // make sure the first server was careful to recycle the connection
1078        assertEquals("This is the first server again!",
1079                readAscii(server.getUrl("/").openStream(), Integer.MAX_VALUE));
1080
1081        RecordedRequest first = server.takeRequest();
1082        assertContains(first.getHeaders(), "Host: " + hostName + ":" + server.getPort());
1083        RecordedRequest second = server2.takeRequest();
1084        assertContains(second.getHeaders(), "Host: " + hostName + ":" + server2.getPort());
1085        RecordedRequest third = server.takeRequest();
1086        assertEquals("Expected connection reuse", 1, third.getSequenceNumber());
1087
1088        server2.shutdown();
1089    }
1090
1091    public void testHttpsWithCustomTrustManager() throws Exception {
1092        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
1093        RecordingTrustManager trustManager = new RecordingTrustManager();
1094        SSLContext sc = SSLContext.getInstance("TLS");
1095        sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());
1096
1097        HostnameVerifier defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
1098        HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
1099        SSLSocketFactory defaultSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
1100        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
1101        try {
1102            TestSSLContext testSSLContext = TestSSLContext.create();
1103            server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
1104            server.enqueue(new MockResponse().setBody("ABC"));
1105            server.enqueue(new MockResponse().setBody("DEF"));
1106            server.enqueue(new MockResponse().setBody("GHI"));
1107            server.play();
1108
1109            URL url = server.getUrl("/");
1110            assertEquals("ABC", readAscii(url.openStream(), Integer.MAX_VALUE));
1111            assertEquals("DEF", readAscii(url.openStream(), Integer.MAX_VALUE));
1112            assertEquals("GHI", readAscii(url.openStream(), Integer.MAX_VALUE));
1113
1114            assertEquals(Arrays.asList("verify " + hostName), hostnameVerifier.calls);
1115            assertEquals(Arrays.asList("checkServerTrusted ["
1116                    + "CN=" + hostName + " 1, "
1117                    + "CN=Test Intermediate Certificate Authority 1, "
1118                    + "CN=Test Root Certificate Authority 1"
1119                    + "] RSA"),
1120                    trustManager.calls);
1121        } finally {
1122            HttpsURLConnection.setDefaultHostnameVerifier(defaultHostnameVerifier);
1123            HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory);
1124        }
1125    }
1126
1127    public void testConnectTimeouts() throws IOException {
1128        StuckServer ss = new StuckServer();
1129        int serverPort = ss.getLocalPort();
1130        URLConnection urlConnection = new URL("http://localhost:" + serverPort).openConnection();
1131        int timeout = 1000;
1132        urlConnection.setConnectTimeout(timeout);
1133        long start = System.currentTimeMillis();
1134        try {
1135            urlConnection.getInputStream();
1136            fail();
1137        } catch (SocketTimeoutException expected) {
1138            long actual = System.currentTimeMillis() - start;
1139            assertTrue(Math.abs(timeout - actual) < 500);
1140        } finally {
1141            ss.close();
1142        }
1143    }
1144
1145    public void testReadTimeouts() throws IOException {
1146        /*
1147         * This relies on the fact that MockWebServer doesn't close the
1148         * connection after a response has been sent. This causes the client to
1149         * try to read more bytes than are sent, which results in a timeout.
1150         */
1151        MockResponse timeout = new MockResponse()
1152                .setBody("ABC")
1153                .clearHeaders()
1154                .addHeader("Content-Length: 4");
1155        server.enqueue(timeout);
1156        server.enqueue(new MockResponse().setBody("unused")); // to keep the server alive
1157        server.play();
1158
1159        URLConnection urlConnection = server.getUrl("/").openConnection();
1160        urlConnection.setReadTimeout(1000);
1161        InputStream in = urlConnection.getInputStream();
1162        assertEquals('A', in.read());
1163        assertEquals('B', in.read());
1164        assertEquals('C', in.read());
1165        try {
1166            in.read(); // if Content-Length was accurate, this would return -1 immediately
1167            fail();
1168        } catch (SocketTimeoutException expected) {
1169        }
1170    }
1171
1172    public void testSetChunkedEncodingAsRequestProperty() throws IOException, InterruptedException {
1173        server.enqueue(new MockResponse());
1174        server.play();
1175
1176        HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
1177        urlConnection.setRequestProperty("Transfer-encoding", "chunked");
1178        urlConnection.setDoOutput(true);
1179        urlConnection.getOutputStream().write("ABC".getBytes("UTF-8"));
1180        assertEquals(200, urlConnection.getResponseCode());
1181
1182        RecordedRequest request = server.takeRequest();
1183        assertEquals("ABC", new String(request.getBody(), "UTF-8"));
1184    }
1185
1186    public void testConnectionCloseInRequest() throws IOException, InterruptedException {
1187        server.enqueue(new MockResponse()); // server doesn't honor the connection: close header!
1188        server.enqueue(new MockResponse());
1189        server.play();
1190
1191        HttpURLConnection a = (HttpURLConnection) server.getUrl("/").openConnection();
1192        a.setRequestProperty("Connection", "close");
1193        assertEquals(200, a.getResponseCode());
1194
1195        HttpURLConnection b = (HttpURLConnection) server.getUrl("/").openConnection();
1196        assertEquals(200, b.getResponseCode());
1197
1198        assertEquals(0, server.takeRequest().getSequenceNumber());
1199        assertEquals("When connection: close is used, each request should get its own connection",
1200                0, server.takeRequest().getSequenceNumber());
1201    }
1202
1203    public void testConnectionCloseInResponse() throws IOException, InterruptedException {
1204        server.enqueue(new MockResponse().addHeader("Connection: close"));
1205        server.enqueue(new MockResponse());
1206        server.play();
1207
1208        HttpURLConnection a = (HttpURLConnection) server.getUrl("/").openConnection();
1209        assertEquals(200, a.getResponseCode());
1210
1211        HttpURLConnection b = (HttpURLConnection) server.getUrl("/").openConnection();
1212        assertEquals(200, b.getResponseCode());
1213
1214        assertEquals(0, server.takeRequest().getSequenceNumber());
1215        assertEquals("When connection: close is used, each request should get its own connection",
1216                0, server.takeRequest().getSequenceNumber());
1217    }
1218
1219    public void testConnectionCloseWithRedirect() throws IOException, InterruptedException {
1220        MockResponse response = new MockResponse()
1221                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
1222                .addHeader("Location: /foo")
1223                .addHeader("Connection: close");
1224        server.enqueue(response);
1225        server.enqueue(new MockResponse().setBody("This is the new location!"));
1226        server.play();
1227
1228        URLConnection connection = server.getUrl("/").openConnection();
1229        assertEquals("This is the new location!",
1230                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1231
1232        assertEquals(0, server.takeRequest().getSequenceNumber());
1233        assertEquals("When connection: close is used, each request should get its own connection",
1234                0, server.takeRequest().getSequenceNumber());
1235    }
1236
1237    public void testResponseCodeDisagreesWithHeaders() throws IOException, InterruptedException {
1238        server.enqueue(new MockResponse()
1239                .setResponseCode(HttpURLConnection.HTTP_NO_CONTENT)
1240                .setBody("This body is not allowed!"));
1241        server.play();
1242
1243        URLConnection connection = server.getUrl("/").openConnection();
1244        assertEquals("This body is not allowed!",
1245                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1246    }
1247
1248    public void testSingleByteReadIsSigned() throws IOException {
1249        server.enqueue(new MockResponse().setBody(new byte[] { -2, -1 }));
1250        server.play();
1251
1252        URLConnection connection = server.getUrl("/").openConnection();
1253        InputStream in = connection.getInputStream();
1254        assertEquals(254, in.read());
1255        assertEquals(255, in.read());
1256        assertEquals(-1, in.read());
1257    }
1258
1259    public void testFlushAfterStreamTransmittedWithChunkedEncoding() throws IOException {
1260        testFlushAfterStreamTransmitted(TransferKind.CHUNKED);
1261    }
1262
1263    public void testFlushAfterStreamTransmittedWithFixedLength() throws IOException {
1264        testFlushAfterStreamTransmitted(TransferKind.FIXED_LENGTH);
1265    }
1266
1267    public void testFlushAfterStreamTransmittedWithNoLengthHeaders() throws IOException {
1268        testFlushAfterStreamTransmitted(TransferKind.END_OF_STREAM);
1269    }
1270
1271    /**
1272     * We explicitly permit apps to close the upload stream even after it has
1273     * been transmitted.  We also permit flush so that buffered streams can
1274     * do a no-op flush when they are closed. http://b/3038470
1275     */
1276    private void testFlushAfterStreamTransmitted(TransferKind transferKind) throws IOException {
1277        server.enqueue(new MockResponse().setBody("abc"));
1278        server.play();
1279
1280        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
1281        connection.setDoOutput(true);
1282        byte[] upload = "def".getBytes("UTF-8");
1283
1284        if (transferKind == TransferKind.CHUNKED) {
1285            connection.setChunkedStreamingMode(0);
1286        } else if (transferKind == TransferKind.FIXED_LENGTH) {
1287            connection.setFixedLengthStreamingMode(upload.length);
1288        }
1289
1290        OutputStream out = connection.getOutputStream();
1291        out.write(upload);
1292        assertEquals("abc", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
1293
1294        out.flush(); // dubious but permitted
1295        try {
1296            out.write("ghi".getBytes("UTF-8"));
1297            fail();
1298        } catch (IOException expected) {
1299        }
1300    }
1301
1302    public void testGetHeadersThrows() throws IOException {
1303        server.enqueue(new MockResponse().setSocketPolicy(DISCONNECT_AT_START));
1304        server.play();
1305
1306        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
1307        try {
1308            connection.getInputStream();
1309            fail();
1310        } catch (IOException expected) {
1311        }
1312
1313        try {
1314            connection.getInputStream();
1315            fail();
1316        } catch (IOException expected) {
1317        }
1318    }
1319
1320    public void testGetKeepAlive() throws Exception {
1321        MockWebServer server = new MockWebServer();
1322        server.enqueue(new MockResponse().setBody("ABC"));
1323        server.play();
1324
1325        // The request should work once and then fail
1326        URLConnection connection = server.getUrl("").openConnection();
1327        InputStream input = connection.getInputStream();
1328        assertEquals("ABC", readAscii(input, Integer.MAX_VALUE));
1329        input.close();
1330        try {
1331            server.getUrl("").openConnection().getInputStream();
1332            fail();
1333        } catch (ConnectException expected) {
1334        }
1335    }
1336
1337    /**
1338     * This test goes through the exhaustive set of interesting ASCII characters
1339     * because most of those characters are interesting in some way according to
1340     * RFC 2396 and RFC 2732. http://b/1158780
1341     */
1342    public void testLenientUrlToUri() throws Exception {
1343        // alphanum
1344        testUrlToUriMapping("abzABZ09", "abzABZ09", "abzABZ09", "abzABZ09", "abzABZ09");
1345
1346        // control characters
1347        testUrlToUriMapping("\u0001", "%01", "%01", "%01", "%01");
1348        testUrlToUriMapping("\u001f", "%1F", "%1F", "%1F", "%1F");
1349
1350        // ascii characters
1351        testUrlToUriMapping("%20", "%20", "%20", "%20", "%20");
1352        testUrlToUriMapping("%20", "%20", "%20", "%20", "%20");
1353        testUrlToUriMapping(" ", "%20", "%20", "%20", "%20");
1354        testUrlToUriMapping("!", "!", "!", "!", "!");
1355        testUrlToUriMapping("\"", "%22", "%22", "%22", "%22");
1356        testUrlToUriMapping("#", null, null, null, "%23");
1357        testUrlToUriMapping("$", "$", "$", "$", "$");
1358        testUrlToUriMapping("&", "&", "&", "&", "&");
1359        testUrlToUriMapping("'", "'", "'", "'", "'");
1360        testUrlToUriMapping("(", "(", "(", "(", "(");
1361        testUrlToUriMapping(")", ")", ")", ")", ")");
1362        testUrlToUriMapping("*", "*", "*", "*", "*");
1363        testUrlToUriMapping("+", "+", "+", "+", "+");
1364        testUrlToUriMapping(",", ",", ",", ",", ",");
1365        testUrlToUriMapping("-", "-", "-", "-", "-");
1366        testUrlToUriMapping(".", ".", ".", ".", ".");
1367        testUrlToUriMapping("/", null, "/", "/", "/");
1368        testUrlToUriMapping(":", null, ":", ":", ":");
1369        testUrlToUriMapping(";", ";", ";", ";", ";");
1370        testUrlToUriMapping("<", "%3C", "%3C", "%3C", "%3C");
1371        testUrlToUriMapping("=", "=", "=", "=", "=");
1372        testUrlToUriMapping(">", "%3E", "%3E", "%3E", "%3E");
1373        testUrlToUriMapping("?", null, null, "?", "?");
1374        testUrlToUriMapping("@", "@", "@", "@", "@");
1375        testUrlToUriMapping("[", null, "%5B", null, "%5B");
1376        testUrlToUriMapping("\\", "%5C", "%5C", "%5C", "%5C");
1377        testUrlToUriMapping("]", null, "%5D", null, "%5D");
1378        testUrlToUriMapping("^", "%5E", "%5E", "%5E", "%5E");
1379        testUrlToUriMapping("_", "_", "_", "_", "_");
1380        testUrlToUriMapping("`", "%60", "%60", "%60", "%60");
1381        testUrlToUriMapping("{", "%7B", "%7B", "%7B", "%7B");
1382        testUrlToUriMapping("|", "%7C", "%7C", "%7C", "%7C");
1383        testUrlToUriMapping("}", "%7D", "%7D", "%7D", "%7D");
1384        testUrlToUriMapping("~", "~", "~", "~", "~");
1385        testUrlToUriMapping("~", "~", "~", "~", "~");
1386        testUrlToUriMapping("\u007f", "%7F", "%7F", "%7F", "%7F");
1387
1388        // beyond ascii
1389        testUrlToUriMapping("\u0080", "%C2%80", "%C2%80", "%C2%80", "%C2%80");
1390        testUrlToUriMapping("\u20ac", "\u20ac", "\u20ac", "\u20ac", "\u20ac");
1391        testUrlToUriMapping("\ud842\udf9f",
1392                "\ud842\udf9f", "\ud842\udf9f", "\ud842\udf9f", "\ud842\udf9f");
1393    }
1394
1395    public void testLenientUrlToUriNul() throws Exception {
1396        testUrlToUriMapping("\u0000", "%00", "%00", "%00", "%00"); // RI fails this
1397    }
1398
1399    private void testUrlToUriMapping(String string, String asAuthority, String asFile,
1400            String asQuery, String asFragment) throws Exception {
1401        if (asAuthority != null) {
1402            assertEquals("http://host" + asAuthority + ".tld/",
1403                    backdoorUrlToUri(new URL("http://host" + string + ".tld/")).toString());
1404        }
1405        if (asFile != null) {
1406            assertEquals("http://host.tld/file" + asFile + "/",
1407                    backdoorUrlToUri(new URL("http://host.tld/file" + string + "/")).toString());
1408        }
1409        if (asQuery != null) {
1410            assertEquals("http://host.tld/file?q" + asQuery + "=x",
1411                    backdoorUrlToUri(new URL("http://host.tld/file?q" + string + "=x")).toString());
1412        }
1413        assertEquals("http://host.tld/file#" + asFragment + "-x",
1414                backdoorUrlToUri(new URL("http://host.tld/file#" + asFragment + "-x")).toString());
1415    }
1416
1417    /**
1418     * Exercises HttpURLConnection to convert URL to a URI. Unlike URL#toURI,
1419     * HttpURLConnection recovers from URLs with unescaped but unsupported URI
1420     * characters like '{' and '|' by escaping these characters.
1421     */
1422    private URI backdoorUrlToUri(URL url) throws Exception {
1423        final AtomicReference<URI> uriReference = new AtomicReference<URI>();
1424
1425        ResponseCache.setDefault(new ResponseCache() {
1426            @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
1427                return null;
1428            }
1429            @Override public CacheResponse get(URI uri, String requestMethod,
1430                    Map<String, List<String>> requestHeaders) throws IOException {
1431                uriReference.set(uri);
1432                throw new UnsupportedOperationException();
1433            }
1434        });
1435
1436        try {
1437            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
1438            connection.getResponseCode();
1439        } catch (Exception expected) {
1440        }
1441
1442        return uriReference.get();
1443    }
1444
1445    /**
1446     * Don't explode if the cache returns a null body. http://b/3373699
1447     */
1448    public void testResponseCacheReturnsNullOutputStream() throws Exception {
1449        final AtomicBoolean aborted = new AtomicBoolean();
1450        ResponseCache.setDefault(new ResponseCache() {
1451            @Override public CacheResponse get(URI uri, String requestMethod,
1452                    Map<String, List<String>> requestHeaders) throws IOException {
1453                return null;
1454            }
1455            @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
1456                return new CacheRequest() {
1457                    @Override public void abort() {
1458                        aborted.set(true);
1459                    }
1460                    @Override public OutputStream getBody() throws IOException {
1461                        return null;
1462                    }
1463                };
1464            }
1465        });
1466
1467        server.enqueue(new MockResponse().setBody("abcdef"));
1468        server.play();
1469
1470        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
1471        InputStream in = connection.getInputStream();
1472        assertEquals("abc", readAscii(in, 3));
1473        in.close();
1474        assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here
1475    }
1476
1477    /**
1478     * http://code.google.com/p/android/issues/detail?id=14562
1479     */
1480    public void testReadAfterLastByte() throws Exception {
1481        server.enqueue(new MockResponse()
1482                .setBody("ABC")
1483                .clearHeaders()
1484                .addHeader("Connection: close")
1485                .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
1486        server.play();
1487
1488        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
1489        InputStream in = connection.getInputStream();
1490        assertEquals("ABC", readAscii(in, 3));
1491        assertEquals(-1, in.read());
1492        assertEquals(-1, in.read()); // throws IOException in Gingerbread
1493    }
1494
1495    /**
1496     * Returns a gzipped copy of {@code bytes}.
1497     */
1498    public byte[] gzip(byte[] bytes) throws IOException {
1499        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
1500        OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
1501        gzippedOut.write(bytes);
1502        gzippedOut.close();
1503        return bytesOut.toByteArray();
1504    }
1505
1506    /**
1507     * Reads at most {@code limit} characters from {@code in} and asserts that
1508     * content equals {@code expected}.
1509     */
1510    private void assertContent(String expected, URLConnection connection, int limit)
1511            throws IOException {
1512        connection.connect();
1513        assertEquals(expected, readAscii(connection.getInputStream(), limit));
1514        ((HttpURLConnection) connection).disconnect();
1515    }
1516
1517    private void assertContent(String expected, URLConnection connection) throws IOException {
1518        assertContent(expected, connection, Integer.MAX_VALUE);
1519    }
1520
1521    private void assertContains(List<String> headers, String header) {
1522        assertTrue(headers.toString(), headers.contains(header));
1523    }
1524
1525    private void assertContainsNoneMatching(List<String> headers, String pattern) {
1526        for (String header : headers) {
1527            if (header.matches(pattern)) {
1528                fail("Header " + header + " matches " + pattern);
1529            }
1530        }
1531    }
1532
1533    private Set<String> newSet(String... elements) {
1534        return new HashSet<String>(Arrays.asList(elements));
1535    }
1536
1537    enum TransferKind {
1538        CHUNKED() {
1539            @Override void setBody(MockResponse response, byte[] content, int chunkSize)
1540                    throws IOException {
1541                response.setChunkedBody(content, chunkSize);
1542            }
1543        },
1544        FIXED_LENGTH() {
1545            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
1546                response.setBody(content);
1547            }
1548        },
1549        END_OF_STREAM() {
1550            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
1551                response.setBody(content);
1552                response.setSocketPolicy(DISCONNECT_AT_END);
1553                for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
1554                    if (h.next().startsWith("Content-Length:")) {
1555                        h.remove();
1556                        break;
1557                    }
1558                }
1559            }
1560        };
1561
1562        abstract void setBody(MockResponse response, byte[] content, int chunkSize)
1563                throws IOException;
1564
1565        void setBody(MockResponse response, String content, int chunkSize) throws IOException {
1566            setBody(response, content.getBytes("UTF-8"), chunkSize);
1567        }
1568    }
1569
1570    enum ProxyConfig {
1571        NO_PROXY() {
1572            @Override public HttpURLConnection connect(MockWebServer server, URL url)
1573                    throws IOException {
1574                return (HttpURLConnection) url.openConnection(Proxy.NO_PROXY);
1575            }
1576        },
1577
1578        CREATE_ARG() {
1579            @Override public HttpURLConnection connect(MockWebServer server, URL url)
1580                    throws IOException {
1581                return (HttpURLConnection) url.openConnection(server.toProxyAddress());
1582            }
1583        },
1584
1585        PROXY_SYSTEM_PROPERTY() {
1586            @Override public HttpURLConnection connect(MockWebServer server, URL url)
1587                    throws IOException {
1588                System.setProperty("proxyHost", "localhost");
1589                System.setProperty("proxyPort", Integer.toString(server.getPort()));
1590                return (HttpURLConnection) url.openConnection();
1591            }
1592        },
1593
1594        HTTP_PROXY_SYSTEM_PROPERTY() {
1595            @Override public HttpURLConnection connect(MockWebServer server, URL url)
1596                    throws IOException {
1597                System.setProperty("http.proxyHost", "localhost");
1598                System.setProperty("http.proxyPort", Integer.toString(server.getPort()));
1599                return (HttpURLConnection) url.openConnection();
1600            }
1601        },
1602
1603        HTTPS_PROXY_SYSTEM_PROPERTY() {
1604            @Override public HttpURLConnection connect(MockWebServer server, URL url)
1605                    throws IOException {
1606                System.setProperty("https.proxyHost", "localhost");
1607                System.setProperty("https.proxyPort", Integer.toString(server.getPort()));
1608                return (HttpURLConnection) url.openConnection();
1609            }
1610        };
1611
1612        public abstract HttpURLConnection connect(MockWebServer server, URL url) throws IOException;
1613    }
1614
1615    private static class RecordingTrustManager implements X509TrustManager {
1616        private final List<String> calls = new ArrayList<String>();
1617
1618        public X509Certificate[] getAcceptedIssuers() {
1619            calls.add("getAcceptedIssuers");
1620            return new X509Certificate[] {};
1621        }
1622
1623        public void checkClientTrusted(X509Certificate[] chain, String authType)
1624                throws CertificateException {
1625            calls.add("checkClientTrusted " + certificatesToString(chain) + " " + authType);
1626        }
1627
1628        public void checkServerTrusted(X509Certificate[] chain, String authType)
1629                throws CertificateException {
1630            calls.add("checkServerTrusted " + certificatesToString(chain) + " " + authType);
1631        }
1632
1633        private String certificatesToString(X509Certificate[] certificates) {
1634            List<String> result = new ArrayList<String>();
1635            for (X509Certificate certificate : certificates) {
1636                result.add(certificate.getSubjectDN() + " " + certificate.getSerialNumber());
1637            }
1638            return result.toString();
1639        }
1640    }
1641
1642    private static class RecordingHostnameVerifier implements HostnameVerifier {
1643        private final List<String> calls = new ArrayList<String>();
1644
1645        public boolean verify(String hostname, SSLSession session) {
1646            calls.add("verify " + hostname);
1647            return true;
1648        }
1649    }
1650}
1651