/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package libcore.java.net;

import com.android.okhttp.AndroidShimResponseCache;
import com.android.okhttp.internal.Platform;
import com.android.okhttp.internal.tls.TrustRootIndex;
import com.google.mockwebserver.Dispatcher;
import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.MockWebServer;
import com.google.mockwebserver.QueueDispatcher;
import com.google.mockwebserver.RecordedRequest;
import com.google.mockwebserver.SocketPolicy;

import java.lang.reflect.Field;
import java.net.ContentHandler;
import java.net.ContentHandlerFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Authenticator;
import java.net.CacheRequest;
import java.net.CacheResponse;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.HttpRetryException;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.PasswordAuthentication;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.ResponseCache;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import javax.net.SocketFactory;
import javax.net.ssl.HandshakeCompletedListener;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import libcore.content.type.MimeMap;
import libcore.java.security.TestKeyStore;
import libcore.javax.net.ssl.TestSSLContext;
import libcore.testing.io.TestIoUtils;

import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END;
import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_START;
import static com.google.mockwebserver.SocketPolicy.FAIL_HANDSHAKE;
import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_INPUT_AT_END;
import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_OUTPUT_AT_END;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public final class URLConnectionTest {

    private MockWebServer server;
    private AndroidShimResponseCache cache;
    private String hostName;
    private List<TestSSLContext> testSSLContextsToClose;

    @Before public void setUp() throws Exception {
        server = new MockWebServer();
        hostName = server.getHostName();
        testSSLContextsToClose = new ArrayList<>();
    }

    @After public void tearDown() throws Exception {
        ResponseCache.setDefault(null);
        Authenticator.setDefault(null);
        System.clearProperty("proxyHost");
        System.clearProperty("proxyPort");
        System.clearProperty("http.proxyHost");
        System.clearProperty("http.proxyPort");
        System.clearProperty("https.proxyHost");
        System.clearProperty("https.proxyPort");
        server.shutdown();
        server = null;
        if (cache != null) {
            cache.delete();
            cache = null;
        }
        for (TestSSLContext testSSLContext : testSSLContextsToClose) {
            testSSLContext.close();
        }
    }

    @Test public void requestHeaderValidation() throws Exception {
        // Android became more strict after M about which characters were allowed in request header
        // names and values: previously almost anything was allowed if it didn't contain \0.

        assertForbiddenRequestHeaderName(null);
        assertForbiddenRequestHeaderName("");
        assertForbiddenRequestHeaderName("\n");
        assertForbiddenRequestHeaderName("a\nb");
        assertForbiddenRequestHeaderName("\u0000");
        assertForbiddenRequestHeaderName("\r");
        assertForbiddenRequestHeaderName("\t");
        assertForbiddenRequestHeaderName("\u001f");
        assertForbiddenRequestHeaderName("\u007f");
        assertForbiddenRequestHeaderName("\u0080");
        assertForbiddenRequestHeaderName("\ud83c\udf69");

        assertEquals(null, setAndReturnRequestHeaderValue(null));
        assertEquals("", setAndReturnRequestHeaderValue(""));
        assertForbiddenRequestHeaderValue("\u0000");

        // Workaround for http://b/26422335 , http://b/26889631 , http://b/27606665 :
        // allow (but strip) trailing \n, \r and \r\n
        // assertForbiddenRequestHeaderValue("\r");
        // End of workaround

        // '\t' in header values can either be (a) forbidden or (b) allowed.
        // The original version of Android N (API 23) implemented behavior
        // (a), but OEMs can backport a fix that changes the behavior to (b).
        // Therefore, this test has been relaxed for Android N CTS to allow
        // either behavior. It is planned that future versions of Android only
        // allow behavior (b).
        try {
            // throws IAE in case (a), passes in case (b)
            assertEquals("a valid\tvalue", setAndReturnRequestHeaderValue("a valid\tvalue"));
        } catch (IllegalArgumentException tolerated) {
            // verify case (a)
            assertForbiddenRequestHeaderValue("\t");
        }
        assertForbiddenRequestHeaderValue("\u001f");
        assertForbiddenRequestHeaderValue("\u007f");

        // For http://b/28867041 the allowed character range was changed.
        // assertForbiddenRequestHeaderValue("\u0080");
        // assertForbiddenRequestHeaderValue("\ud83c\udf69");
        assertEquals("\u0080", setAndReturnRequestHeaderValue("\u0080"));
        assertEquals("\ud83c\udf69", setAndReturnRequestHeaderValue("\ud83c\udf69"));

        // Workaround for http://b/26422335 , http://b/26889631 , http://b/27606665 :
        // allow (but strip) trailing \n, \r and \r\n
        assertEquals("", setAndReturnRequestHeaderValue("\n"));
        assertEquals("a", setAndReturnRequestHeaderValue("a\n"));
        assertEquals("", setAndReturnRequestHeaderValue("\r"));
        assertEquals("a", setAndReturnRequestHeaderValue("a\r"));
        assertEquals("", setAndReturnRequestHeaderValue("\r\n"));
        assertEquals("a", setAndReturnRequestHeaderValue("a\r\n"));
        assertForbiddenRequestHeaderValue("a\nb");
        assertForbiddenRequestHeaderValue("\nb");
        assertForbiddenRequestHeaderValue("a\rb");
        assertForbiddenRequestHeaderValue("\rb");
        assertForbiddenRequestHeaderValue("a\r\nb");
        assertForbiddenRequestHeaderValue("\r\nb");
        // End of workaround
    }

    private static void assertForbiddenRequestHeaderName(String name) throws Exception {
        URL url = new URL("http://www.google.com/");
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        try {
            urlConnection.addRequestProperty(name, "value");
            fail("Expected exception");
        } catch (IllegalArgumentException expected) {
        } catch (NullPointerException expectedIfNull) {
            assertTrue(name == null);
        }
    }

    private static void assertForbiddenRequestHeaderValue(String value) throws Exception {
        URL url = new URL("http://www.google.com/");
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        try {
            urlConnection.addRequestProperty("key", value);
            fail("Expected exception");
        } catch (IllegalArgumentException expected) {
        }
    }

    private static String setAndReturnRequestHeaderValue(String value) throws Exception {
        URL url = new URL("http://www.google.com/");
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.addRequestProperty("key", value);
        return urlConnection.getRequestProperty("key");
    }

    @Test public void requestHeaders() throws IOException, InterruptedException {
        server.enqueue(new MockResponse());
        server.play();

        HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
        urlConnection.addRequestProperty("D", "e");
        urlConnection.addRequestProperty("D", "f");
        assertEquals("f", urlConnection.getRequestProperty("D"));
        assertEquals("f", urlConnection.getRequestProperty("d"));
        Map<String, List<String>> requestHeaders = urlConnection.getRequestProperties();
        assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("D")));
        assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("d")));
        try {
            requestHeaders.put("G", Arrays.asList("h"));
            fail("Modified an unmodifiable view.");
        } catch (UnsupportedOperationException expected) {
        }
        try {
            requestHeaders.get("D").add("i");
            fail("Modified an unmodifiable view.");
        } catch (UnsupportedOperationException expected) {
        }
        try {
            urlConnection.setRequestProperty(null, "j");
            fail();
        } catch (NullPointerException expected) {
        }
        try {
            urlConnection.addRequestProperty(null, "k");
            fail();
        } catch (NullPointerException expected) {
        }
        urlConnection.setRequestProperty("NullValue", null); // should fail silently!
        assertNull(urlConnection.getRequestProperty("NullValue"));
        urlConnection.addRequestProperty("AnotherNullValue", null);  // should fail silently!
        assertNull(urlConnection.getRequestProperty("AnotherNullValue"));

        urlConnection.getResponseCode();
        RecordedRequest request = server.takeRequest();
        assertContains(request.getHeaders(), "D: e");
        assertContains(request.getHeaders(), "D: f");
        assertContainsNoneMatching(request.getHeaders(), "NullValue.*");
        assertContainsNoneMatching(request.getHeaders(), "AnotherNullValue.*");
        assertContainsNoneMatching(request.getHeaders(), "G:.*");
        assertContainsNoneMatching(request.getHeaders(), "null:.*");

        try {
            urlConnection.addRequestProperty("N", "o");
            fail("Set header after connect");
        } catch (IllegalStateException expected) {
        }
        try {
            urlConnection.setRequestProperty("P", "q");
            fail("Set header after connect");
        } catch (IllegalStateException expected) {
        }
        try {
            urlConnection.getRequestProperties();
            fail();
        } catch (IllegalStateException expected) {
        }
    }

    @Test public void getRequestPropertyReturnsLastValue() throws Exception {
        server.play();
        HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
        urlConnection.addRequestProperty("A", "value1");
        urlConnection.addRequestProperty("A", "value2");
        assertEquals("value2", urlConnection.getRequestProperty("A"));
    }

    @Test public void responseHeaders() throws IOException, InterruptedException {
        server.enqueue(new MockResponse()
                .setStatus("HTTP/1.0 200 Fantastic")
                .addHeader("A: c")
                .addHeader("B: d")
                .addHeader("A: e")
                .setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8));
        server.play();

        HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals(200, urlConnection.getResponseCode());
        assertEquals("Fantastic", urlConnection.getResponseMessage());
        assertEquals("HTTP/1.0 200 Fantastic", urlConnection.getHeaderField(null));
        Map<String, List<String>> responseHeaders = urlConnection.getHeaderFields();
        assertEquals(Arrays.asList("HTTP/1.0 200 Fantastic"), responseHeaders.get(null));
        assertEquals(newSet("c", "e"), new HashSet<String>(responseHeaders.get("A")));
        assertEquals(newSet("c", "e"), new HashSet<String>(responseHeaders.get("a")));
        try {
            responseHeaders.put("N", Arrays.asList("o"));
            fail("Modified an unmodifiable view.");
        } catch (UnsupportedOperationException expected) {
        }
        try {
            responseHeaders.get("A").add("f");
            fail("Modified an unmodifiable view.");
        } catch (UnsupportedOperationException expected) {
        }
        assertEquals("A", urlConnection.getHeaderFieldKey(0));
        assertEquals("c", urlConnection.getHeaderField(0));
        assertEquals("B", urlConnection.getHeaderFieldKey(1));
        assertEquals("d", urlConnection.getHeaderField(1));
        assertEquals("A", urlConnection.getHeaderFieldKey(2));
        assertEquals("e", urlConnection.getHeaderField(2));
    }

    @Test public void getErrorStreamOnSuccessfulRequest() throws Exception {
        server.enqueue(new MockResponse().setBody("A"));
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        assertNull(connection.getErrorStream());
    }

    @Test public void getErrorStreamOnUnsuccessfulRequest() throws Exception {
        server.enqueue(new MockResponse().setResponseCode(404).setBody("A"));
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals("A", readAscii(connection.getErrorStream()));
    }

    // Check that if we don't read to the end of a response, the next request on the
    // recycled connection doesn't get the unread tail of the first request's response.
    // http://code.google.com/p/android/issues/detail?id=2939
    @Test public void bug2939() throws Exception {
        MockResponse response = new MockResponse().setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8);

        server.enqueue(response);
        server.enqueue(response);
        server.play();

        assertContent("ABCDE", server.getUrl("/").openConnection(), 5);
        assertContent("ABCDE", server.getUrl("/").openConnection(), 5);
    }

    // Check that we recognize a few basic mime types by extension.
    // http://code.google.com/p/android/issues/detail?id=10100
    @Test public void bug10100() throws Exception {
        assertEquals("image/jpeg", URLConnection.guessContentTypeFromName("someFile.jpg"));
        assertEquals("application/pdf", URLConnection.guessContentTypeFromName("stuff.pdf"));
    }

    @Test public void connectionsArePooled() throws Exception {
        MockResponse response = new MockResponse().setBody("ABCDEFGHIJKLMNOPQR");

        server.enqueue(response);
        server.enqueue(response);
        server.enqueue(response);
        server.play();

        assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/foo").openConnection());
        assertEquals(0, server.takeRequest().getSequenceNumber());
        assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/bar?baz=quux").openConnection());
        assertEquals(1, server.takeRequest().getSequenceNumber());
        assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/z").openConnection());
        assertEquals(2, server.takeRequest().getSequenceNumber());
    }

    @Test public void chunkedConnectionsArePooled() throws Exception {
        MockResponse response = new MockResponse().setChunkedBody("ABCDEFGHIJKLMNOPQR", 5);

        server.enqueue(response);
        server.enqueue(response);
        server.enqueue(response);
        server.play();

        assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/foo").openConnection());
        assertEquals(0, server.takeRequest().getSequenceNumber());
        assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/bar?baz=quux").openConnection());
        assertEquals(1, server.takeRequest().getSequenceNumber());
        assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/z").openConnection());
        assertEquals(2, server.takeRequest().getSequenceNumber());
    }

    /**
     * Test that connections are added to the pool as soon as the response has
     * been consumed.
     */
    @Test public void connectionsArePooledWithoutExplicitDisconnect() throws Exception {
        server.enqueue(new MockResponse().setBody("ABC"));
        server.enqueue(new MockResponse().setBody("DEF"));
        server.play();

        URLConnection connection1 = server.getUrl("/").openConnection();
        assertEquals("ABC", readAscii(connection1.getInputStream()));
        assertEquals(0, server.takeRequest().getSequenceNumber());
        URLConnection connection2 = server.getUrl("/").openConnection();
        assertEquals("DEF", readAscii(connection2.getInputStream()));
        assertEquals(1, server.takeRequest().getSequenceNumber());
    }

    @Test public void serverClosesSocket() throws Exception {
        testServerClosesSocket(DISCONNECT_AT_END);
    }

    @Test public void serverShutdownInput() throws Exception {
        testServerClosesSocket(SHUTDOWN_INPUT_AT_END);
    }

    private void testServerClosesSocket(SocketPolicy socketPolicy) throws Exception {
        server.enqueue(new MockResponse()
                .setBody("This connection won't pool properly")
                .setSocketPolicy(socketPolicy));
        server.enqueue(new MockResponse().setBody("This comes after a busted connection"));
        server.play();

        assertContent("This connection won't pool properly", server.getUrl("/a").openConnection());
        assertEquals(0, server.takeRequest().getSequenceNumber());
        assertContent("This comes after a busted connection", server.getUrl("/b").openConnection());
        // sequence number 0 means the HTTP socket connection was not reused
        assertEquals(0, server.takeRequest().getSequenceNumber());
    }

    @Test public void serverShutdownOutput() throws Exception {
        // This test causes MockWebServer to log a "connection failed" stack trace

        // Setting the server workerThreads to 1 ensures the responses are generated in the order
        // the requests are accepted by the server. Without this the second and third requests made
        // by the client (the request for "/b" and the retry for "/b" when the bad socket is
        // detected) can be handled by the server out of order leading to test failure.
        server.setWorkerThreads(1);
        server.enqueue(new MockResponse()
                .setBody("Output shutdown after this response")
                .setSocketPolicy(SHUTDOWN_OUTPUT_AT_END));
        server.enqueue(new MockResponse().setBody("This response will fail to write"));
        server.enqueue(new MockResponse().setBody("This comes after a busted connection"));
        server.play();

        assertContent("Output shutdown after this response", server.getUrl("/a").openConnection());
        assertEquals(0, server.takeRequest().getSequenceNumber());
        assertContent("This comes after a busted connection", server.getUrl("/b").openConnection());
        assertEquals(1, server.takeRequest().getSequenceNumber());
        assertEquals(0, server.takeRequest().getSequenceNumber());
    }

    enum WriteKind { BYTE_BY_BYTE, SMALL_BUFFERS, LARGE_BUFFERS }

    @Test public void chunkedUpload_byteByByte() throws Exception {
        doUpload(TransferKind.CHUNKED, WriteKind.BYTE_BY_BYTE);
    }

    @Test public void chunkedUpload_smallBuffers() throws Exception {
        doUpload(TransferKind.CHUNKED, WriteKind.SMALL_BUFFERS);
    }

    @Test public void chunkedUpload_largeBuffers() throws Exception {
        doUpload(TransferKind.CHUNKED, WriteKind.LARGE_BUFFERS);
    }

    @Test public void fixedLengthUpload_byteByByte() throws Exception {
        doUpload(TransferKind.FIXED_LENGTH, WriteKind.BYTE_BY_BYTE);
    }

    @Test public void fixedLengthUpload_smallBuffers() throws Exception {
        doUpload(TransferKind.FIXED_LENGTH, WriteKind.SMALL_BUFFERS);
    }

    @Test public void fixedLengthUpload_largeBuffers() throws Exception {
        doUpload(TransferKind.FIXED_LENGTH, WriteKind.LARGE_BUFFERS);
    }

    private void doUpload(TransferKind uploadKind, WriteKind writeKind) throws Exception {
        int n = 512*1024;
        server.setBodyLimit(0);
        server.enqueue(new MockResponse());
        server.play();

        HttpURLConnection conn = (HttpURLConnection) server.getUrl("/").openConnection();
        conn.setDoOutput(true);
        conn.setRequestMethod("POST");
        if (uploadKind == TransferKind.CHUNKED) {
            conn.setChunkedStreamingMode(-1);
        } else {
            conn.setFixedLengthStreamingMode(n);
        }
        OutputStream out = conn.getOutputStream();
        if (writeKind == WriteKind.BYTE_BY_BYTE) {
            for (int i = 0; i < n; ++i) {
                out.write('x');
            }
        } else {
            byte[] buf = new byte[writeKind == WriteKind.SMALL_BUFFERS ? 256 : 64*1024];
            Arrays.fill(buf, (byte) 'x');
            for (int i = 0; i < n; i += buf.length) {
                out.write(buf, 0, Math.min(buf.length, n - i));
            }
        }
        out.close();
        assertEquals(200, conn.getResponseCode());
        RecordedRequest request = server.takeRequest();
        assertEquals(n, request.getBodySize());
        if (uploadKind == TransferKind.CHUNKED) {
            assertTrue(request.getChunkSizes().size() > 0);
        } else {
            assertTrue(request.getChunkSizes().isEmpty());
        }
    }

    @Test public void getResponseCodeNoResponseBody() throws Exception {
        server.enqueue(new MockResponse()
                .addHeader("abc: def"));
        server.play();

        URL url = server.getUrl("/");
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setDoInput(false);
        assertEquals("def", conn.getHeaderField("abc"));
        assertEquals(200, conn.getResponseCode());
        try {
            conn.getInputStream();
            fail();
        } catch (ProtocolException expected) {
        }
    }

    @Test public void connectViaHttps() throws IOException, InterruptedException {
        checkConnectViaHttps();
    }

    private void checkConnectViaHttps() throws IOException, InterruptedException {
        TestSSLContext testSSLContext = createDefaultTestSSLContext();

        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
        server.play();

        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/foo").openConnection();
        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());

        assertContent("this response comes via HTTPS", connection);

        RecordedRequest request = server.takeRequest();
        assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
        assertEquals("TLSv1.2", request.getSslProtocol());
    }

    @Test public void connectViaHttpsReusingConnections() throws IOException, InterruptedException {
        TestSSLContext testSSLContext = createDefaultTestSSLContext();
        SSLSocketFactory clientSocketFactory = testSSLContext.clientContext.getSocketFactory();

        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
        server.enqueue(new MockResponse().setBody("another response via HTTPS"));
        server.play();

        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
        connection.setSSLSocketFactory(clientSocketFactory);
        assertContent("this response comes via HTTPS", connection);

        connection = (HttpsURLConnection) server.getUrl("/").openConnection();
        connection.setSSLSocketFactory(clientSocketFactory);
        assertContent("another response via HTTPS", connection);

        assertEquals(0, server.takeRequest().getSequenceNumber());
        assertEquals(1, server.takeRequest().getSequenceNumber());
    }

    @Test public void connectViaHttpsReusingConnectionsDifferentFactories()
            throws IOException, InterruptedException {
        TestSSLContext testSSLContext = createDefaultTestSSLContext();

        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
        server.enqueue(new MockResponse().setBody("another response via HTTPS"));
        server.play();

        // install a custom SSL socket factory so the server can be authorized
        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
        assertContent("this response comes via HTTPS", connection);

        connection = (HttpsURLConnection) server.getUrl("/").openConnection();
        try {
            readAscii(connection.getInputStream());
            fail("without an SSL socket factory, the connection should fail");
        } catch (SSLException expected) {
        }
    }

    /**
     * Verify that we don't retry connections on certificate verification errors.
     *
     * http://code.google.com/p/android/issues/detail?id=13178
     */
    @Test public void connectViaHttpsToUntrustedServer() throws IOException, InterruptedException {
        TestSSLContext testSSLContext = TestSSLContext.create(TestKeyStore.getClientCA2(),
                                                              TestKeyStore.getServer());
        testSSLContextsToClose.add(testSSLContext);

        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
        server.enqueue(new MockResponse()); // unused
        server.play();

        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/foo").openConnection();
        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
        try {
            connection.getInputStream();
            fail();
        } catch (SSLHandshakeException expected) {
            assertTrue(expected.getCause() instanceof CertificateException);
        }
        assertEquals(0, server.getRequestCount());
    }

    @Test public void connectViaProxy_emptyPath() throws Exception {
        // expected normalization http://android -> http://android/ per b/30107354
        checkConnectViaProxy(
                ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY, "http://android.com",
                "http://android.com/", "android.com");
    }

    @Test public void connectViaProxy_complexUrlWithNoPath() throws Exception {
        checkConnectViaProxy(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY,
                "http://android.com:8080?height=100&width=42",
                "http://android.com:8080/?height=100&width=42",
                "android.com:8080");
    }

    @Test public void connectViaProxyUsingProxyArg() throws Exception {
        checkConnectViaProxy(ProxyConfig.CREATE_ARG);
    }

    @Test public void connectViaProxyUsingProxySystemProperty() throws Exception {
        checkConnectViaProxy(ProxyConfig.PROXY_SYSTEM_PROPERTY);
    }

    @Test public void connectViaProxyUsingHttpProxySystemProperty() throws Exception {
        checkConnectViaProxy(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
    }

    private void checkConnectViaProxy(ProxyConfig proxyConfig) throws Exception {
        checkConnectViaProxy(proxyConfig,
                "http://android.com/foo", "http://android.com/foo", "android.com");
    }

    private void checkConnectViaProxy(ProxyConfig proxyConfig, String urlString,
            String expectedUrlInRequestLine, String expectedHost) throws Exception {
        MockResponse mockResponse = new MockResponse().setBody("this response comes via a proxy");
        server.enqueue(mockResponse);
        server.play();

        URL url = new URL(urlString);
        HttpURLConnection connection = proxyConfig.connect(server, url);
        assertContent("this response comes via a proxy", connection);

        RecordedRequest request = server.takeRequest();
        assertEquals("GET " + expectedUrlInRequestLine + " HTTP/1.1", request.getRequestLine());
        assertContains(request.getHeaders(), "Host: " + expectedHost);
    }

    @Test public void contentDisagreesWithContentLengthHeader() throws IOException {
        server.enqueue(new MockResponse()
                .setBody("abc\r\nYOU SHOULD NOT SEE THIS")
                .clearHeaders()
                .addHeader("Content-Length: 3"));
        server.play();

        assertContent("abc", server.getUrl("/").openConnection());
    }

    @Test public void contentDisagreesWithChunkedHeader() throws IOException {
        MockResponse mockResponse = new MockResponse();
        mockResponse.setChunkedBody("abc", 3);
        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
        bytesOut.write(mockResponse.getBody());
        bytesOut.write("\r\nYOU SHOULD NOT SEE THIS".getBytes());
        mockResponse.setBody(bytesOut.toByteArray());
        mockResponse.clearHeaders();
        mockResponse.addHeader("Transfer-encoding: chunked");

        server.enqueue(mockResponse);
        server.play();

        assertContent("abc", server.getUrl("/").openConnection());
    }

    @Test public void connectViaHttpProxyToHttpsUsingProxyArgWithNoProxy() throws Exception {
        checkConnectViaDirectProxyToHttps(ProxyConfig.NO_PROXY);
    }

    @Test public void connectViaHttpProxyToHttpsUsingHttpProxySystemProperty() throws Exception {
        // https should not use http proxy
        checkConnectViaDirectProxyToHttps(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
    }

    private void checkConnectViaDirectProxyToHttps(ProxyConfig proxyConfig) throws Exception {
        TestSSLContext testSSLContext = createDefaultTestSSLContext();

        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
        server.play();

        URL url = server.getUrl("/foo");
        HttpsURLConnection connection = (HttpsURLConnection) proxyConfig.connect(server, url);
        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());

        assertContent("this response comes via HTTPS", connection);

        RecordedRequest request = server.takeRequest();
        assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
    }


    @Test public void connectViaHttpProxyToHttpsUsingProxyArg() throws Exception {
        checkConnectViaHttpProxyToHttps(ProxyConfig.CREATE_ARG);
    }

    /**
     * We weren't honoring all of the appropriate proxy system properties when
     * connecting via HTTPS. http://b/3097518
     */
    @Test public void connectViaHttpProxyToHttpsUsingProxySystemProperty() throws Exception {
        checkConnectViaHttpProxyToHttps(ProxyConfig.PROXY_SYSTEM_PROPERTY);
    }

    @Test public void connectViaHttpProxyToHttpsUsingHttpsProxySystemProperty() throws Exception {
        checkConnectViaHttpProxyToHttps(ProxyConfig.HTTPS_PROXY_SYSTEM_PROPERTY);
    }

    /**
     * We were verifying the wrong hostname when connecting to an HTTPS site
     * through a proxy. http://b/3097277
     */
    private void checkConnectViaHttpProxyToHttps(ProxyConfig proxyConfig) throws Exception {
        TestSSLContext testSSLContext = createDefaultTestSSLContext();
        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();

        server.useHttps(testSSLContext.serverContext.getSocketFactory(), true);
        server.enqueue(new MockResponse()
                .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
                .clearHeaders());
        server.enqueue(new MockResponse().setBody("this response comes via a secure proxy"));
        server.play();

        URL url = new URL("https://android.com/foo");
        HttpsURLConnection connection = (HttpsURLConnection) proxyConfig.connect(server, url);
        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
        connection.setHostnameVerifier(hostnameVerifier);

        assertContent("this response comes via a secure proxy", connection);

        RecordedRequest connect = server.takeRequest();
        assertEquals("Connect line failure on proxy",
                "CONNECT android.com:443 HTTP/1.1", connect.getRequestLine());
        assertContains(connect.getHeaders(), "Host: android.com:443");

        RecordedRequest get = server.takeRequest();
        assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
        assertContains(get.getHeaders(), "Host: android.com");
        assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
    }


    /**
     * Tolerate bad https proxy response when using HttpResponseCache. http://b/6754912
     */
    @Test public void connectViaHttpProxyToHttpsUsingBadProxyAndHttpResponseCache()
            throws Exception {
        TestSSLContext testSSLContext = createDefaultTestSSLContext();

        initResponseCache();

        server.useHttps(testSSLContext.serverContext.getSocketFactory(), true);

        // The inclusion of a body in the response to the CONNECT is key to reproducing b/6754912.
        MockResponse badProxyResponse = new MockResponse()
                .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
                .clearHeaders()
                .setBody("bogus proxy connect response content");

        server.enqueue(badProxyResponse);
        server.enqueue(new MockResponse().setBody("response"));

        server.play();

        URL url = new URL("https://android.com/foo");
        ProxyConfig proxyConfig = ProxyConfig.PROXY_SYSTEM_PROPERTY;
        HttpsURLConnection connection = (HttpsURLConnection) proxyConfig.connect(server, url);
        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
        connection.setHostnameVerifier(new RecordingHostnameVerifier());
        assertContent("response", connection);

        RecordedRequest connect = server.takeRequest();
        assertEquals("CONNECT android.com:443 HTTP/1.1", connect.getRequestLine());
        assertContains(connect.getHeaders(), "Host: android.com:443");
    }

    private void initResponseCache() throws IOException {
        String tmp = System.getProperty("java.io.tmpdir");
        File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID());
        cache = AndroidShimResponseCache.create(cacheDir, Integer.MAX_VALUE);
        ResponseCache.setDefault(cache);
    }

    /**
     * Test Etag headers are returned correctly when a client-side cache is not installed.
     * https://code.google.com/p/android/issues/detail?id=108949
     */
    @Test public void etagHeaders_uncached() throws Exception {
        final String etagValue1 = "686897696a7c876b7e";
        final String body1 = "Response with etag 1";
        final String etagValue2 = "686897696a7c876b7f";
        final String body2 = "Response with etag 2";

        server.enqueue(
            new MockResponse()
                .setBody(body1)
                .setHeader("Content-Type", "text/plain")
                .setHeader("Etag", etagValue1));
        server.enqueue(
            new MockResponse()
                .setBody(body2)
                .setHeader("Content-Type", "text/plain")
                .setHeader("Etag", etagValue2));
        server.play();

        URL url = server.getUrl("/");
        HttpURLConnection connection1 = (HttpURLConnection) url.openConnection();
        assertEquals(etagValue1, connection1.getHeaderField("Etag"));
        assertContent(body1, connection1);
        connection1.disconnect();

        // Discard the server-side record of the request made.
        server.takeRequest();

        HttpURLConnection connection2 = (HttpURLConnection) url.openConnection();
        assertEquals(etagValue2, connection2.getHeaderField("Etag"));
        assertContent(body2, connection2);
        connection2.disconnect();

        // Check the client did not cache.
        RecordedRequest request = server.takeRequest();
        assertNull(request.getHeader("If-None-Match"));
    }

    @Test public void getFileNameMap_null() {
        assertThrowsNpe(() -> URLConnection.getFileNameMap().getContentTypeFor(null));
        assertThrowsNpe(() -> URLConnection.guessContentTypeFromName(null));
    }

    private static void assertThrowsNpe(Runnable runnable) {
        try {
            runnable.run();
            fail();
        } catch (NullPointerException expected) {
        }
    }

    /**
     * Checks that paths ending in '/' (directory listings) are identified as HTML.
     */
    @Test public void getFileNameMap_directory() {
        checkFileNameMap("text/html", "/directory/path/");
        checkFileNameMap("text/html", "http://example.com/path/");
        checkFileNameMap("text/html", "http://example.com/path/#fragment");
    }

    @Test public void getFileNameMap_simple() {
        checkFileNameMap("text/plain", "example.txt");
        checkFileNameMap("text/plain", "example.com/file.txt");
    }

    @Test public void getFileNameMap_compositeExtension() {
        checkFileNameMap("text/plain", "example.html.txt"); // html.txt isn't known, but txt is
        checkFileNameMap("application/x-font-pcf", "filename.pcf.Z"); // pcf.Z is known
    }

    @Test public void getFileNameMap_compositeExtension_customMimeMap() {
        MimeMap testMimeMap = MimeMap.builder()
                .addMimeMapping("text/html", "html")
                .addMimeMapping("application/gzip", "gz")
                .addMimeMapping("application/tar+gzip", "tar.gz")
                .build();
        MimeMap defaultMimeMap = MimeMap.getDefault();
        MimeMap.setDefaultSupplier(() -> testMimeMap);
        try {
            checkFileNameMap("application/gzip", "filename.gz");
            checkFileNameMap("application/gzip", "filename.foobar.gz");
            checkFileNameMap("application/gzip", "filename.html.gz");
            checkFileNameMap("application/tar+gzip", "filename.tar.gz"); // tar.gz is found
        } finally {
            MimeMap.setDefaultSupplier(() -> defaultMimeMap);
        }
    }

    /**
     * Checks that as long as there are no '.' or '/', a file name that matches
     * a known extension is interpreted as that extension, as if it was preceded by ".".
     */
    @Test public void getFileNameMap_plainExtension() {
        checkFileNameMap("text/plain", "txt");
        checkFileNameMap("text/plain", "txt#fragment");
        checkFileNameMap(null, "example.com/txt");
        checkFileNameMap(null, "example.com/txt#fragment");
        checkFileNameMap(null, "example/txt");
        checkFileNameMap(null, "http://example/txt#fragment");
    }

    /**
     * Checks cases where there's a '.' in the fragment, path or as an earlier path
     * of a file name (only the last '.' that is part of the filename should count).
     */
    @Test public void getFileNameMap_dotsInOtherPlaces() {
        // '.' in path
        checkFileNameMap("text/html", "example.com/foo.txt/bar.html");
        checkFileNameMap("text/plain", "example.com/foo.html/bar.txt");
        checkFileNameMap(null, "/path.txt/noextensionfound");

        // '.' earlier in filename
        checkFileNameMap("text/plain", "example.html.txt");

        // '.' in fragment
        checkFileNameMap(null, "/path/noextensionfound#fragment.html");

        // multiple additional dots that shouldn't count
        checkFileNameMap("text/plain", "/path.html/foo/../readme.html.txt#fragment.html");
    }

    @Test public void getFileNameMap_multipleHashCharacters() {
        // Based on RFC 3986, URLs can only contain a single '#' but android.net.Uri
        // considers the fragment to start after the first, rather than the last, '#'.
        // We check that FileNameMap cuts off after the first '#', consistent with Uri.
        checkFileNameMap(null, "/path/noextensionfound#frag.txt#ment.html");
    }

    /**
     * Checks that fragments are stripped when determining file extension.
     */
    @Test public void getFileNameMap_fragment() {
        checkFileNameMap("text/plain", "example.txt#fragment");
        checkFileNameMap("text/plain", "example.com/path/example.txt#fragment");
    }

    /**
     * Checks that fragments are NOT stripped and therefore break recognition
     * of file type.
     * This matches RI behavior, but it'd be reasonable to change behavior here.
     */
    @Test public void getFileNameMap_queryParameter() {
        checkFileNameMap(null, "example.txt?key=value");
        checkFileNameMap(null, "example.txt?key=value#fragment");
    }

    private static void checkFileNameMap(String expected, String fileName) {
        String actual = URLConnection.getFileNameMap().getContentTypeFor(fileName);
        assertEquals(fileName, expected, actual);

        // The documentation doesn't guarantee that these two do exactly the same thing,
        // but it is one reasonable way to implement it.
        assertEquals(fileName, expected, URLConnection.guessContentTypeFromName(fileName));
    }

    /**
     * Test Etag headers are returned correctly when a client-side cache is installed and the server
     * data is unchanged.
     * https://code.google.com/p/android/issues/detail?id=108949
     */
    @Test public void etagHeaders_cachedWithServerHit() throws Exception {
        final String etagValue = "686897696a7c876b7e";
        final String body = "Response with etag";

        server.enqueue(
            new MockResponse()
                .setBody(body)
                .setResponseCode(HttpURLConnection.HTTP_OK)
                .setHeader("Content-Type", "text/plain")
                .setHeader("Etag", etagValue));

        server.enqueue(
            new MockResponse()
                .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
        server.play();

        initResponseCache();

        URL url = server.getUrl("/");
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        assertEquals(etagValue, connection.getHeaderField("Etag"));
        assertContent(body, connection);
        connection.disconnect();

        // Discard the server-side record of the request made.
        server.takeRequest();

        // Confirm the cached body is returned along with the original etag header.
        HttpURLConnection cachedConnection = (HttpURLConnection) url.openConnection();
        assertEquals(etagValue, cachedConnection.getHeaderField("Etag"));
        assertContent(body, cachedConnection);
        cachedConnection.disconnect();

        // Check the client formatted the request correctly.
        RecordedRequest request = server.takeRequest();
        assertEquals(etagValue, request.getHeader("If-None-Match"));
    }

    /**
     * Test Etag headers are returned correctly when a client-side cache is installed and the server
     * data has changed.
     * https://code.google.com/p/android/issues/detail?id=108949
     */
    @Test public void etagHeaders_cachedWithServerMiss() throws Exception {
        final String etagValue1 = "686897696a7c876b7e";
        final String body1 = "Response with etag 1";
        final String etagValue2 = "686897696a7c876b7f";
        final String body2 = "Response with etag 2";

        server.enqueue(
            new MockResponse()
                .setBody(body1)
                .setResponseCode(HttpURLConnection.HTTP_OK)
                .setHeader("Content-Type", "text/plain")
                .setHeader("Etag", etagValue1));

        server.enqueue(
            new MockResponse()
                .setBody(body2)
                .setResponseCode(HttpURLConnection.HTTP_OK)
                .setHeader("Content-Type", "text/plain")
                .setHeader("Etag", etagValue2));

        server.play();

        initResponseCache();

        URL url = server.getUrl("/");
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        assertEquals(etagValue1, connection.getHeaderField("Etag"));
        assertContent(body1, connection);
        connection.disconnect();

        // Discard the server-side record of the request made.
        server.takeRequest();

        // Confirm the new body is returned along with the new etag header.
        HttpURLConnection cachedConnection = (HttpURLConnection) url.openConnection();
        assertEquals(etagValue2, cachedConnection.getHeaderField("Etag"));
        assertContent(body2, cachedConnection);
        cachedConnection.disconnect();

        // Check the client formatted the request correctly.
        RecordedRequest request = server.takeRequest();
        assertEquals(etagValue1, request.getHeader("If-None-Match"));
    }

    /**
     * Test which headers are sent unencrypted to the HTTP proxy.
     */
    @Test public void proxyConnectIncludesProxyHeadersOnly()
            throws IOException, InterruptedException {
        Authenticator.setDefault(new SimpleAuthenticator());
        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
        TestSSLContext testSSLContext = createDefaultTestSSLContext();

        server.useHttps(testSSLContext.serverContext.getSocketFactory(), true);
        server.enqueue(new MockResponse()
                .setResponseCode(407)
                .addHeader("Proxy-Authenticate: Basic realm=\"localhost\""));
        server.enqueue(new MockResponse()
                .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
                .clearHeaders());
        server.enqueue(new MockResponse().setBody("encrypted response from the origin server"));
        server.play();

        URL url = new URL("https://android.com/foo");
        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(
                server.toProxyAddress());
        connection.addRequestProperty("Private", "Secret");
        connection.addRequestProperty("User-Agent", "baz");
        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
        connection.setHostnameVerifier(hostnameVerifier);
        assertContent("encrypted response from the origin server", connection);

        // connect1 and connect2 are tunnel requests which potentially tunnel multiple requests;
        // Thus we can't expect its headers to exactly match those of the wrapped request.
        // See https://github.com/square/okhttp/commit/457fb428a729c50c562822571ea9b13e689648f3

        {
            RecordedRequest connect1 = server.takeRequest();
            List<String> headers = connect1.getHeaders();
            assertContainsNoneMatching(headers, "Private.*");
            assertContainsNoneMatching(headers, "Proxy\\-Authorization.*");
            assertHeaderPresent(connect1, "User-Agent");
            assertContains(headers, "Host: android.com:443");
            assertContains(headers, "Proxy-Connection: Keep-Alive");
        }

        {
            RecordedRequest connect2 = server.takeRequest();
            List<String> headers = connect2.getHeaders();
            assertContainsNoneMatching(headers, "Private.*");
            assertHeaderPresent(connect2, "Proxy-Authorization");
            assertHeaderPresent(connect2, "User-Agent");
            assertContains(headers, "Host: android.com:443");
            assertContains(headers, "Proxy-Connection: Keep-Alive");
        }

        RecordedRequest get = server.takeRequest();
        assertContains(get.getHeaders(), "Private: Secret");
        assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
    }

    @Test public void proxyAuthenticateOnConnect() throws Exception {
        Authenticator.setDefault(new SimpleAuthenticator());
        TestSSLContext testSSLContext = createDefaultTestSSLContext();
        server.useHttps(testSSLContext.serverContext.getSocketFactory(), true);
        server.enqueue(new MockResponse()
                .setResponseCode(407)
                .addHeader("Proxy-Authenticate: Basic realm=\"localhost\""));
        server.enqueue(new MockResponse()
                .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
                .clearHeaders());
        server.enqueue(new MockResponse().setBody("A"));
        server.play();

        URL url = new URL("https://android.com/foo");
        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(
                server.toProxyAddress());
        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
        connection.setHostnameVerifier(new RecordingHostnameVerifier());
        assertContent("A", connection);

        RecordedRequest connect1 = server.takeRequest();
        assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine());
        assertContainsNoneMatching(connect1.getHeaders(), "Proxy\\-Authorization.*");

        RecordedRequest connect2 = server.takeRequest();
        assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine());
        assertContains(connect2.getHeaders(), "Proxy-Authorization: Basic "
                + SimpleAuthenticator.BASE_64_CREDENTIALS);

        RecordedRequest get = server.takeRequest();
        assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
        assertContainsNoneMatching(get.getHeaders(), "Proxy\\-Authorization.*");
    }

    // Don't disconnect after building a tunnel with CONNECT
    // http://code.google.com/p/android/issues/detail?id=37221
    @Test public void proxyWithConnectionClose() throws IOException {
        TestSSLContext testSSLContext = createDefaultTestSSLContext();
        server.useHttps(testSSLContext.serverContext.getSocketFactory(), true);
        server.enqueue(new MockResponse()
                .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
                .clearHeaders());
        server.enqueue(new MockResponse().setBody("this response comes via a proxy"));
        server.play();

        URL url = new URL("https://android.com/foo");
        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(
                server.toProxyAddress());
        connection.setRequestProperty("Connection", "close");
        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
        connection.setHostnameVerifier(new RecordingHostnameVerifier());

        assertContent("this response comes via a proxy", connection);
    }

    @Test public void disconnectedConnection() throws IOException {
        server.enqueue(new MockResponse()
                .throttleBody(2, 100, TimeUnit.MILLISECONDS)
                .setBody("ABCD"));
        server.play();

        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        InputStream in = connection.getInputStream();
        assertEquals('A', (char) in.read());
        connection.disconnect();
        try {
            // Reading 'B' may succeed if it's buffered.
            in.read();
            // But 'C' shouldn't be buffered (the response is throttled) and this should fail.
            in.read();
            fail("Expected a connection closed exception");
        } catch (IOException expected) {
        }
    }

    @Test public void disconnectFromBackgroundThread_blockedRead_beforeHeader()
            throws IOException {
        QueueDispatcher dispatcher = new QueueDispatcher() {
            @Override
            public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
                Thread.sleep(6000);
                return super.dispatch(request);
            }
        };
        server.setDispatcher(dispatcher);
        server.enqueue(new MockResponse().setHeader("Key", "Value").setBody("Response body"));
        checkDisconnectFromBackgroundThread_blockedRead(2000, null /* disconnectMillis */);
    }

    @Test public void disconnectFromBackgroundThread_blockedRead_beforeBody()
            throws IOException {
        server.enqueue(new MockResponse().setHeader("Key", "Value")
                .setBody("Response body").setBodyDelayTimeMs(6000));
        checkDisconnectFromBackgroundThread_blockedRead(2000, "" /* disconnectMillis */);
    }

    /**
     *
     * @throws IOException
     */
    @Test public void disconnectFromBackgroundThread_blockedRead_duringBody()
            throws IOException {
        server.enqueue(new MockResponse().setHeader("Key", "Value")
                .setBody("Response body").throttleBody(3, 1333, TimeUnit.MILLISECONDS));
        // After 2 sec, we should have read about 6 bytes (we sleep 1333msec after every 3 bytes).
        checkDisconnectFromBackgroundThread_blockedRead(2000, "Respon");
    }

    /**
     * Checks that {@link HttpURLConnection#disconnect() disconnecting} a blocked read
     * from a background thread unblocks the reading thread quickly and that the headers/body
     * read so far are as given.
     *
     * The disconnect happens after approximately {@code disconnectMillis} msec (between half
     * and double that is tolerated), so the server must already be set up such that reading
     * the headers and the entire request takes comfortably more than that, eg.
     * {@code 3 * disconnectMillis}.
     *
     * @param disconnectMillis number of milliseconds until the connection should be
     *        {@link HttpURLConnection#disconnect() disconnected} by a background thread.
     * @param expectedResponseContent The part of the body that is expected to have been read by
     *        the time the connection is disconnected, or null if not even the headers
     *        are expected to have been read at the time.
     * @throws IOException if one occurs unexpectedly while establishing the connection.
     */
    private void checkDisconnectFromBackgroundThread_blockedRead(
            long disconnectMillis, String expectedResponseContent) throws IOException {
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();

        Thread disconnectThread = new Thread("Disconnect after " + disconnectMillis + "msec") {
            @Override
            public void run() {
                try {
                    Thread.sleep(disconnectMillis);
                } catch (InterruptedException e) {
                    // Even if an AssertionFailedError on this background thread doesn't
                    // cause the test to fail directly, we'd still prematurely disconnect()
                    // and that would (if significant) be detected further down by the
                    // assertion on the number of elapsed milliseconds observed by the
                    // main thread.
                    fail("Unexpectedly interrupted: " + e);
                }
                connection.disconnect();
            }
        };

        ByteArrayOutputStream auditStream = new ByteArrayOutputStream();
        AuditInputStream inputStream = null;
        boolean headerRead = false;
        long start = System.currentTimeMillis();
        disconnectThread.start();
        try {
            inputStream = new AuditInputStream(connection.getInputStream(), auditStream);
            connection.getHeaderFields();
            headerRead = true;
            readAscii(inputStream);
            fail("Didn't expect to successfully read all of the data");
        } catch (IOException expected) {
        } finally {
            TestIoUtils.closeQuietly(inputStream);
        }
        long elapsed = System.currentTimeMillis() - start;

        assertTrue("Expected approx. " + disconnectMillis + " msec elapsed, got " + elapsed,
                disconnectMillis / 2 <= elapsed && elapsed <= 2 * disconnectMillis);
        String readBody = new String(auditStream.toByteArray(), StandardCharsets.UTF_8);

        String actualResponse = headerRead ? readBody : null;
        assertEquals("Headers read: " + headerRead + "; read response body: " +  readBody,
                expectedResponseContent, actualResponse);
    }


    // http://b/33763156
    @Test public void disconnectDuringConnect_getInputStream() throws IOException {
        checkDisconnectDuringConnect(HttpURLConnection::getInputStream);
    }

    // http://b/33763156
    @Test public void disconnectDuringConnect_getOutputStream() throws IOException {
        checkDisconnectDuringConnect(HttpURLConnection::getOutputStream);
    }

    // http://b/33763156
    @Test public void disconnectDuringConnect_getResponseCode() throws IOException {
        checkDisconnectDuringConnect(HttpURLConnection::getResponseCode);
    }

    // http://b/33763156
    @Test public void disconnectDuringConnect_getResponseMessage() throws IOException {
        checkDisconnectDuringConnect(HttpURLConnection::getResponseMessage);
    }

    interface ConnectStrategy {
        /**
         * Causes the given {@code connection}, which was previously disconnected,
         * to initiate the connection.
         */
        void connect(HttpURLConnection connection) throws IOException;
    }

    // http://b/33763156
    private void checkDisconnectDuringConnect(ConnectStrategy connectStrategy) throws IOException {
        server.enqueue(new MockResponse().setBody("This should never be sent"));
        server.play();

        final AtomicReference<HttpURLConnection> connectionHolder = new AtomicReference<>();
        class DisconnectingCookieHandler extends CookieManager {
            @Override
            public Map<String, List<String>> get(URI uri, Map<String, List<String>> map)
                    throws IOException {
                Map<String, List<String>> result = super.get(uri, map);
                connectionHolder.get().disconnect();
                return result;
            }
        }
        CookieHandler defaultCookieHandler = CookieHandler.getDefault();
        try {
            CookieHandler.setDefault(new DisconnectingCookieHandler());
            HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
            connectionHolder.set(connection);
            try {
                connectStrategy.connect(connection);
                fail();
            } catch (IOException expected) {
                assertEquals("Canceled", expected.getMessage());
            } finally {
                connection.disconnect();
            }
        } finally {
            CookieHandler.setDefault(defaultCookieHandler);
        }
    }

    @Test public void disconnectBeforeConnect() throws IOException {
        server.enqueue(new MockResponse().setBody("A"));
        server.play();

        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        connection.disconnect();

        assertContent("A", connection);
        assertEquals(200, connection.getResponseCode());
    }

    @Test public void disconnectAfterOnlyResponseCodeCausesNoCloseGuardWarning() throws IOException {
        server.enqueue(new MockResponse()
                .setBody(gzip("ABCABCABC".getBytes("UTF-8")))
                .addHeader("Content-Encoding: gzip"));
        server.play();

        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        try {
            assertEquals(200, connection.getResponseCode());
        } finally {
            connection.disconnect();
        }
    }

    @Test public void defaultRequestProperty() throws Exception {
        URLConnection.setDefaultRequestProperty("X-testSetDefaultRequestProperty", "A");
        assertNull(URLConnection.getDefaultRequestProperty("X-setDefaultRequestProperty"));
    }

    /**
     * Reads {@code count} characters from the stream. If the stream is
     * exhausted before {@code count} characters can be read, the remaining
     * characters are returned and the stream is closed.
     */
    private static String readAscii(InputStream in, int count) throws IOException {
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < count; i++) {
            int value = in.read();
            if (value == -1) {
                in.close();
                break;
            }
            result.append((char) value);
        }
        return result.toString();
    }

    private static String readAscii(InputStream in) throws IOException {
        return readAscii(in, Integer.MAX_VALUE);
    }

    @Test public void markAndResetWithContentLengthHeader() throws IOException {
        testMarkAndReset(TransferKind.FIXED_LENGTH);
    }

    @Test public void markAndResetWithChunkedEncoding() throws IOException {
        testMarkAndReset(TransferKind.CHUNKED);
    }

    @Test public void markAndResetWithNoLengthHeaders() throws IOException {
        testMarkAndReset(TransferKind.END_OF_STREAM);
    }

    private void testMarkAndReset(TransferKind transferKind) throws IOException {
        MockResponse response = new MockResponse();
        transferKind.setBody(response, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1024);
        server.enqueue(response);
        server.enqueue(response);
        server.play();

        InputStream in = server.getUrl("/").openConnection().getInputStream();
        assertFalse("This implementation claims to support mark().", in.markSupported());
        in.mark(5);
        assertEquals("ABCDE", readAscii(in, 5));
        try {
            in.reset();
            fail();
        } catch (IOException expected) {
        }
        assertEquals("FGHIJKLMNOPQRSTUVWXYZ", readAscii(in));
        assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", server.getUrl("/").openConnection());
    }

    /**
     * We've had a bug where we forget the HTTP response when we see response
     * code 401. This causes a new HTTP request to be issued for every call into
     * the URLConnection.
     */
    @Test public void unauthorizedResponseHandling() throws IOException {
        MockResponse response = new MockResponse()
                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
                .setResponseCode(401) // UNAUTHORIZED
                .setBody("Unauthorized");
        server.enqueue(response);
        server.enqueue(response);
        server.enqueue(response);
        server.play();

        URL url = server.getUrl("/");
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();

        assertEquals(401, conn.getResponseCode());
        assertEquals(401, conn.getResponseCode());
        assertEquals(401, conn.getResponseCode());
        assertEquals(1, server.getRequestCount());
    }

    @Test public void nonHexChunkSize() throws IOException {
        server.enqueue(new MockResponse()
                .setBody("5\r\nABCDE\r\nG\r\nFGHIJKLMNOPQRSTU\r\n0\r\n\r\n")
                .clearHeaders()
                .addHeader("Transfer-encoding: chunked"));
        server.play();

        URLConnection connection = server.getUrl("/").openConnection();
        try {
            readAscii(connection.getInputStream());
            fail();
        } catch (IOException e) {
        }
    }

    @Test public void missingChunkBody() throws IOException {
        server.enqueue(new MockResponse()
                .setBody("5")
                .clearHeaders()
                .addHeader("Transfer-encoding: chunked")
                .setSocketPolicy(DISCONNECT_AT_END));
        server.play();

        URLConnection connection = server.getUrl("/").openConnection();
        try {
            readAscii(connection.getInputStream());
            fail();
        } catch (IOException e) {
        }
    }

    /**
     * This test checks whether connections are gzipped by default. This
     * behavior in not required by the API, so a failure of this test does not
     * imply a bug in the implementation.
     */
    @Test public void gzipEncodingEnabledByDefault() throws IOException, InterruptedException {
        server.enqueue(new MockResponse()
                .setBody(gzip("ABCABCABC".getBytes("UTF-8")))
                .addHeader("Content-Encoding: gzip"));
        server.play();

        URLConnection connection = server.getUrl("/").openConnection();
        assertEquals("ABCABCABC", readAscii(connection.getInputStream()));
        assertNull(connection.getContentEncoding());
        assertEquals(-1, connection.getContentLength());

        RecordedRequest request = server.takeRequest();
        assertContains(request.getHeaders(), "Accept-Encoding: gzip");
    }

    @Test public void clientConfiguredGzipContentEncoding() throws Exception {
        byte[] bodyBytes = gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8"));
        server.enqueue(new MockResponse()
                .setBody(bodyBytes)
                .addHeader("Content-Encoding: gzip")
                .addHeader("Content-Length: " + bodyBytes.length));
        server.play();

        URLConnection connection = server.getUrl("/").openConnection();
        connection.addRequestProperty("Accept-Encoding", "gzip");
        InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
        assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", readAscii(gunzippedIn));
        assertEquals(bodyBytes.length, connection.getContentLength());

        RecordedRequest request = server.takeRequest();
        assertContains(request.getHeaders(), "Accept-Encoding: gzip");
        assertEquals("gzip", connection.getContentEncoding());
    }

    @Test public void gzipAndConnectionReuseWithFixedLength() throws Exception {
        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH);
    }

    @Test public void gzipAndConnectionReuseWithChunkedEncoding() throws Exception {
        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED);
    }

    @Test public void clientConfiguredCustomContentEncoding() throws Exception {
        server.enqueue(new MockResponse()
                .setBody("ABCDE")
                .addHeader("Content-Encoding: custom"));
        server.play();

        URLConnection connection = server.getUrl("/").openConnection();
        connection.addRequestProperty("Accept-Encoding", "custom");
        assertEquals("ABCDE", readAscii(connection.getInputStream()));

        RecordedRequest request = server.takeRequest();
        assertContains(request.getHeaders(), "Accept-Encoding: custom");
    }

    /**
     * Test a bug where gzip input streams weren't exhausting the input stream,
     * which corrupted the request that followed.
     * http://code.google.com/p/android/issues/detail?id=7059
     */
    private void testClientConfiguredGzipContentEncodingAndConnectionReuse(
            TransferKind transferKind) throws Exception {
        MockResponse responseOne = new MockResponse();
        responseOne.addHeader("Content-Encoding: gzip");
        transferKind.setBody(responseOne, gzip("one (gzipped)".getBytes("UTF-8")), 5);
        server.enqueue(responseOne);
        MockResponse responseTwo = new MockResponse();
        transferKind.setBody(responseTwo, "two (identity)", 5);
        server.enqueue(responseTwo);
        server.play();

        URLConnection connection = server.getUrl("/").openConnection();
        connection.addRequestProperty("Accept-Encoding", "gzip");
        InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
        assertEquals("one (gzipped)", readAscii(gunzippedIn));
        assertEquals(0, server.takeRequest().getSequenceNumber());

        connection = server.getUrl("/").openConnection();
        assertEquals("two (identity)", readAscii(connection.getInputStream()));
        assertEquals(1, server.takeRequest().getSequenceNumber());
    }

    /**
     * Test that HEAD requests don't have a body regardless of the response
     * headers. http://code.google.com/p/android/issues/detail?id=24672
     */
    @Test public void headAndContentLength() throws Exception {
        server.enqueue(new MockResponse()
                .clearHeaders()
                .addHeader("Content-Length: 100"));
        server.enqueue(new MockResponse().setBody("A"));
        server.play();

        HttpURLConnection connection1 = (HttpURLConnection) server.getUrl("/").openConnection();
        connection1.setRequestMethod("HEAD");
        assertEquals("100", connection1.getHeaderField("Content-Length"));
        assertContent("", connection1);

        HttpURLConnection connection2 = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals("A", readAscii(connection2.getInputStream()));

        assertEquals(0, server.takeRequest().getSequenceNumber());
        assertEquals(1, server.takeRequest().getSequenceNumber());
    }

    /**
     * Test that request body chunking works. This test has been relaxed from treating
     * the {@link java.net.HttpURLConnection#setChunkedStreamingMode(int)}
     * chunk length as being fixed because OkHttp no longer guarantees
     * the fixed chunk size. Instead, we check that chunking takes place
     * and we force the chunk size with flushes.
     */
    @Test public void setChunkedStreamingMode() throws IOException, InterruptedException {
        server.enqueue(new MockResponse());
        server.play();

        HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
        // Later releases of Android ignore the value for chunkLength if it is > 0 and default to
        // a fixed chunkLength. During the change-over period while the chunkLength indicates the
        // chunk buffer size (inc. header) the chunkLength has to be >= 8. This enables the flush()
        // to dictate the size of the chunks.
        urlConnection.setChunkedStreamingMode(50 /* chunkLength */);
        urlConnection.setDoOutput(true);
        OutputStream outputStream = urlConnection.getOutputStream();
        String outputString = "ABCDEFGH";
        byte[] outputBytes = outputString.getBytes("US-ASCII");
        int targetChunkSize = 3;
        for (int i = 0; i < outputBytes.length; i += targetChunkSize) {
            int count = i + targetChunkSize < outputBytes.length ? 3 : outputBytes.length - i;
            outputStream.write(outputBytes, i, count);
            outputStream.flush();
        }
        assertEquals(200, urlConnection.getResponseCode());

        RecordedRequest request = server.takeRequest();
        assertEquals(outputString, new String(request.getBody(), "US-ASCII"));
        assertEquals(Arrays.asList(3, 3, 2), request.getChunkSizes());
    }

    @Test public void authenticateWithFixedLengthStreaming() throws Exception {
        testAuthenticateWithStreamingPost(StreamingMode.FIXED_LENGTH);
    }

    @Test public void authenticateWithChunkedStreaming() throws Exception {
        testAuthenticateWithStreamingPost(StreamingMode.CHUNKED);
    }

    private void testAuthenticateWithStreamingPost(StreamingMode streamingMode) throws Exception {
        MockResponse pleaseAuthenticate = new MockResponse()
                .setResponseCode(401)
                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
                .setBody("Please authenticate.");
        server.enqueue(pleaseAuthenticate);
        server.play();

        Authenticator.setDefault(new SimpleAuthenticator());
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        connection.setDoOutput(true);
        byte[] requestBody = { 'A', 'B', 'C', 'D' };
        if (streamingMode == StreamingMode.FIXED_LENGTH) {
            connection.setFixedLengthStreamingMode(requestBody.length);
        } else if (streamingMode == StreamingMode.CHUNKED) {
            connection.setChunkedStreamingMode(0);
        }
        OutputStream outputStream = connection.getOutputStream();
        outputStream.write(requestBody);
        outputStream.close();
        try {
            connection.getInputStream();
            fail();
        } catch (HttpRetryException expected) {
        }

        // no authorization header for the request...
        RecordedRequest request = server.takeRequest();
        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
        assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
    }

    @Test public void setValidRequestMethod() throws Exception {
        server.play();
        assertValidRequestMethod("GET");
        assertValidRequestMethod("DELETE");
        assertValidRequestMethod("HEAD");
        assertValidRequestMethod("OPTIONS");
        assertValidRequestMethod("POST");
        assertValidRequestMethod("PUT");
        assertValidRequestMethod("TRACE");
    }

    private void assertValidRequestMethod(String requestMethod) throws Exception {
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        connection.setRequestMethod(requestMethod);
        assertEquals(requestMethod, connection.getRequestMethod());
    }

    @Test public void setInvalidRequestMethodLowercase() throws Exception {
        server.play();
        assertInvalidRequestMethod("get");
    }

    @Test public void setInvalidRequestMethodConnect() throws Exception {
        server.play();
        assertInvalidRequestMethod("CONNECT");
    }

    private void assertInvalidRequestMethod(String requestMethod) throws Exception {
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        try {
            connection.setRequestMethod(requestMethod);
            fail();
        } catch (ProtocolException expected) {
        }
    }

    @Test public void cannotSetNegativeFixedLengthStreamingMode() throws Exception {
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        try {
            connection.setFixedLengthStreamingMode(-2);
            fail();
        } catch (IllegalArgumentException expected) {
        }
    }

    @Test public void canSetNegativeChunkedStreamingMode() throws Exception {
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        connection.setChunkedStreamingMode(-2);
    }

    @Test public void cannotSetFixedLengthStreamingModeAfterConnect() throws Exception {
        server.enqueue(new MockResponse().setBody("A"));
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals("A", readAscii(connection.getInputStream()));
        try {
            connection.setFixedLengthStreamingMode(1);
            fail();
        } catch (IllegalStateException expected) {
        }
    }

    @Test public void cannotSetChunkedStreamingModeAfterConnect() throws Exception {
        server.enqueue(new MockResponse().setBody("A"));
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals("A", readAscii(connection.getInputStream()));
        try {
            connection.setChunkedStreamingMode(1);
            fail();
        } catch (IllegalStateException expected) {
        }
    }

    @Test public void cannotSetFixedLengthStreamingModeAfterChunkedStreamingMode() throws Exception {
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        connection.setChunkedStreamingMode(1);
        try {
            connection.setFixedLengthStreamingMode(1);
            fail();
        } catch (IllegalStateException expected) {
        }
    }

    @Test public void cannotSetChunkedStreamingModeAfterFixedLengthStreamingMode()
            throws Exception {
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        connection.setFixedLengthStreamingMode(1);
        try {
            connection.setChunkedStreamingMode(1);
            fail();
        } catch (IllegalStateException expected) {
        }
    }

    @Test public void secureFixedLengthStreaming() throws Exception {
        testSecureStreamingPost(StreamingMode.FIXED_LENGTH);
    }

    @Test public void secureChunkedStreaming() throws Exception {
        testSecureStreamingPost(StreamingMode.CHUNKED);
    }

    /**
     * Users have reported problems using HTTPS with streaming request bodies.
     * http://code.google.com/p/android/issues/detail?id=12860
     */
    private void testSecureStreamingPost(StreamingMode streamingMode) throws Exception {
        TestSSLContext testSSLContext = createDefaultTestSSLContext();
        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
        server.enqueue(new MockResponse().setBody("Success!"));
        server.play();

        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
        connection.setDoOutput(true);
        byte[] requestBody = { 'A', 'B', 'C', 'D' };
        if (streamingMode == StreamingMode.FIXED_LENGTH) {
            connection.setFixedLengthStreamingMode(requestBody.length);
        } else if (streamingMode == StreamingMode.CHUNKED) {
            connection.setChunkedStreamingMode(0);
        }
        OutputStream outputStream = connection.getOutputStream();
        outputStream.write(requestBody);
        outputStream.close();
        assertEquals("Success!", readAscii(connection.getInputStream()));

        RecordedRequest request = server.takeRequest();
        assertEquals("POST / HTTP/1.1", request.getRequestLine());
        if (streamingMode == StreamingMode.FIXED_LENGTH) {
            assertEquals(Collections.<Integer>emptyList(), request.getChunkSizes());
        } else if (streamingMode == StreamingMode.CHUNKED) {
            assertEquals(Arrays.asList(4), request.getChunkSizes());
        }
        assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
    }

    enum StreamingMode {
        FIXED_LENGTH, CHUNKED
    }

    @Test public void authenticateWithPost() throws Exception {
        MockResponse pleaseAuthenticate = new MockResponse()
                .setResponseCode(401)
                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
                .setBody("Please authenticate.");
        // fail auth three times...
        server.enqueue(pleaseAuthenticate);
        server.enqueue(pleaseAuthenticate);
        server.enqueue(pleaseAuthenticate);
        // ...then succeed the fourth time
        server.enqueue(new MockResponse().setBody("Successful auth!"));
        server.play();

        Authenticator.setDefault(new SimpleAuthenticator());
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        connection.setDoOutput(true);
        byte[] requestBody = { 'A', 'B', 'C', 'D' };
        OutputStream outputStream = connection.getOutputStream();
        outputStream.write(requestBody);
        outputStream.close();
        assertEquals("Successful auth!", readAscii(connection.getInputStream()));

        // no authorization header for the first request...
        RecordedRequest request = server.takeRequest();
        assertContainsNoneMatching(request.getHeaders(), "Authorization: .*");

        // ...but the three requests that follow include an authorization header
        for (int i = 0; i < 3; i++) {
            request = server.takeRequest();
            assertEquals("POST / HTTP/1.1", request.getRequestLine());
            assertContains(request.getHeaders(), "Authorization: Basic "
                    + SimpleAuthenticator.BASE_64_CREDENTIALS);
            assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
        }
    }

    @Test public void authenticateWithGet() throws Exception {
        MockResponse pleaseAuthenticate = new MockResponse()
                .setResponseCode(401)
                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
                .setBody("Please authenticate.");
        // fail auth three times...
        server.enqueue(pleaseAuthenticate);
        server.enqueue(pleaseAuthenticate);
        server.enqueue(pleaseAuthenticate);
        // ...then succeed the fourth time
        server.enqueue(new MockResponse().setBody("Successful auth!"));
        server.play();

        SimpleAuthenticator authenticator = new SimpleAuthenticator();
        Authenticator.setDefault(authenticator);
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals("Successful auth!", readAscii(connection.getInputStream()));
        assertEquals(Authenticator.RequestorType.SERVER, authenticator.requestorType);
        assertEquals(server.getPort(), authenticator.requestingPort);
        assertEquals(InetAddress.getByName(server.getHostName()), authenticator.requestingSite);
        assertEquals("protected area", authenticator.requestingPrompt);
        assertEquals("http", authenticator.requestingProtocol);
        assertEquals("Basic", authenticator.requestingScheme);

        // no authorization header for the first request...
        RecordedRequest request = server.takeRequest();
        assertContainsNoneMatching(request.getHeaders(), "Authorization: .*");

        // ...but the three requests that follow requests include an authorization header
        for (int i = 0; i < 3; i++) {
            request = server.takeRequest();
            assertEquals("GET / HTTP/1.1", request.getRequestLine());
            assertContains(request.getHeaders(), "Authorization: Basic "
                    + SimpleAuthenticator.BASE_64_CREDENTIALS);
        }
    }

    // bug 11473660
    @Test public void authenticateWithLowerCaseHeadersAndScheme() throws Exception {
        MockResponse pleaseAuthenticate = new MockResponse()
                .setResponseCode(401)
                .addHeader("www-authenticate: basic realm=\"protected area\"")
                .setBody("Please authenticate.");
        // fail auth three times...
        server.enqueue(pleaseAuthenticate);
        server.enqueue(pleaseAuthenticate);
        server.enqueue(pleaseAuthenticate);
        // ...then succeed the fourth time
        server.enqueue(new MockResponse().setBody("Successful auth!"));
        server.play();

        SimpleAuthenticator authenticator = new SimpleAuthenticator();
        Authenticator.setDefault(authenticator);
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals("Successful auth!", readAscii(connection.getInputStream()));
        assertEquals(Authenticator.RequestorType.SERVER, authenticator.requestorType);
        assertEquals(server.getPort(), authenticator.requestingPort);
        assertEquals(InetAddress.getByName(server.getHostName()), authenticator.requestingSite);
        assertEquals("protected area", authenticator.requestingPrompt);
        assertEquals("http", authenticator.requestingProtocol);
        assertEquals("basic", authenticator.requestingScheme);
    }

    // http://code.google.com/p/android/issues/detail?id=19081
    @Test public void authenticateWithCommaSeparatedAuthenticationMethods() throws Exception {
        server.enqueue(new MockResponse()
                .setResponseCode(401)
                .addHeader("WWW-Authenticate: Scheme1 realm=\"a\", Basic realm=\"b\", "
                        + "Scheme3 realm=\"c\"")
                .setBody("Please authenticate."));
        server.enqueue(new MockResponse().setBody("Successful auth!"));
        server.play();

        SimpleAuthenticator authenticator = new SimpleAuthenticator();
        authenticator.expectedPrompt = "b";
        Authenticator.setDefault(authenticator);
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals("Successful auth!", readAscii(connection.getInputStream()));

        assertContainsNoneMatching(server.takeRequest().getHeaders(), "Authorization: .*");
        assertContains(server.takeRequest().getHeaders(),
                "Authorization: Basic " + SimpleAuthenticator.BASE_64_CREDENTIALS);
        assertEquals("Basic", authenticator.requestingScheme);
    }

    @Test public void authenticateWithMultipleAuthenticationHeaders() throws Exception {
        server.enqueue(new MockResponse()
                .setResponseCode(401)
                .addHeader("WWW-Authenticate: Scheme1 realm=\"a\"")
                .addHeader("WWW-Authenticate: Basic realm=\"b\"")
                .addHeader("WWW-Authenticate: Scheme3 realm=\"c\"")
                .setBody("Please authenticate."));
        server.enqueue(new MockResponse().setBody("Successful auth!"));
        server.play();

        SimpleAuthenticator authenticator = new SimpleAuthenticator();
        authenticator.expectedPrompt = "b";
        Authenticator.setDefault(authenticator);
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals("Successful auth!", readAscii(connection.getInputStream()));

        assertContainsNoneMatching(server.takeRequest().getHeaders(), "Authorization: .*");
        assertContains(server.takeRequest().getHeaders(),
                "Authorization: Basic " + SimpleAuthenticator.BASE_64_CREDENTIALS);
        assertEquals("Basic", authenticator.requestingScheme);
    }

    @Test public void redirectedWithChunkedEncoding() throws Exception {
        testRedirected(TransferKind.CHUNKED, true);
    }

    @Test public void redirectedWithContentLengthHeader() throws Exception {
        testRedirected(TransferKind.FIXED_LENGTH, true);
    }

    @Test public void redirectedWithNoLengthHeaders() throws Exception {
        testRedirected(TransferKind.END_OF_STREAM, false);
    }

    private void testRedirected(TransferKind transferKind, boolean reuse) throws Exception {
        MockResponse response = new MockResponse()
                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
                .addHeader("Location: /foo");
        transferKind.setBody(response, "This page has moved!", 10);
        server.enqueue(response);
        server.enqueue(new MockResponse().setBody("This is the new location!"));
        server.play();

        URLConnection connection = server.getUrl("/").openConnection();
        assertEquals("This is the new location!", readAscii(connection.getInputStream()));

        RecordedRequest first = server.takeRequest();
        assertEquals("GET / HTTP/1.1", first.getRequestLine());
        RecordedRequest retry = server.takeRequest();
        assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
        if (reuse) {
            assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
        }
    }

    @Test public void redirectedOnHttps() throws IOException, InterruptedException {
        TestSSLContext testSSLContext = createDefaultTestSSLContext();
        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
        server.enqueue(new MockResponse()
                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
                .addHeader("Location: /foo")
                .setBody("This page has moved!"));
        server.enqueue(new MockResponse().setBody("This is the new location!"));
        server.play();

        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
        assertEquals("This is the new location!", readAscii(connection.getInputStream()));

        RecordedRequest first = server.takeRequest();
        assertEquals("GET / HTTP/1.1", first.getRequestLine());
        RecordedRequest retry = server.takeRequest();
        assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
        assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
    }

    @Test public void notRedirectedFromHttpsToHttp() throws IOException, InterruptedException {
        TestSSLContext testSSLContext = createDefaultTestSSLContext();
        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
        server.enqueue(new MockResponse()
                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
                .addHeader("Location: http://anyhost/foo")
                .setBody("This page has moved!"));
        server.play();

        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
        assertEquals("This page has moved!", readAscii(connection.getInputStream()));
    }

    @Test public void notRedirectedFromHttpToHttps() throws IOException, InterruptedException {
        server.enqueue(new MockResponse()
                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
                .addHeader("Location: https://anyhost/foo")
                .setBody("This page has moved!"));
        server.play();

        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals("This page has moved!", readAscii(connection.getInputStream()));
    }

    @Test public void redirectToAnotherOriginServer() throws Exception {
        MockWebServer server2 = new MockWebServer();
        server2.enqueue(new MockResponse().setBody("This is the 2nd server!"));
        server2.play();

        server.enqueue(new MockResponse()
                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
                .addHeader("Location: " + server2.getUrl("/").toString())
                .setBody("This page has moved!"));
        server.enqueue(new MockResponse().setBody("This is the first server again!"));
        server.play();

        URLConnection connection = server.getUrl("/").openConnection();
        assertEquals("This is the 2nd server!", readAscii(connection.getInputStream()));
        assertEquals(server2.getUrl("/"), connection.getURL());

        // make sure the first server was careful to recycle the connection
        assertEquals("This is the first server again!", readAscii(server.getUrl("/").openStream()));

        RecordedRequest first = server.takeRequest();
        assertContains(first.getHeaders(), "Host: " + hostName + ":" + server.getPort());
        RecordedRequest second = server2.takeRequest();
        assertContains(second.getHeaders(), "Host: " + hostName + ":" + server2.getPort());
        RecordedRequest third = server.takeRequest();
        assertEquals("Expected connection reuse", 1, third.getSequenceNumber());

        server2.shutdown();
    }

    // http://b/27590872 - assert we do not throw a runtime exception if a server responds with
    // a location that cannot be represented directly by URI.
    @Test public void redirectWithInvalidRedirectUrl() throws Exception {
        // The first server hosts a redirect to a second. We need two so that the ProxySelector
        // installed is used for the redirect. Otherwise the second request will be handled via the
        // existing keep-alive connection.
        server.play();

        MockWebServer server2 = new MockWebServer();
        server2.play();

        String targetPath = "/target";
        // The "%0" in the suffix is invalid without a second digit.
        String invalidSuffix = "?foo=%0&bar=%00";

        String redirectPath = server2.getUrl(targetPath).toString();
        String invalidRedirectUri = redirectPath + invalidSuffix;

        // Redirect to the invalid URI.
        server.enqueue(new MockResponse()
                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
                .addHeader("Location: " + invalidRedirectUri));

        server2.enqueue(new MockResponse().setBody("Target"));

        // Assert the target URI is actually invalid.
        try {
            new URI(invalidRedirectUri);
            fail("Target URL is expected to be invalid");
        } catch (URISyntaxException expected) {}

        // The ProxySelector requires a URI object, which forces the HttpURLConnectionImpl to create
        // a URI object containing a string based on the redirect address, regardless of what it is
        // using internally to hold the target address.
        ProxySelector originalSelector = ProxySelector.getDefault();
        final List<URI> proxySelectorUris = new ArrayList<>();
        ProxySelector.setDefault(new ProxySelector() {
            @Override
            public List<Proxy> select(URI uri) {
                if (uri.getScheme().equals("http")) {
                    // Ignore socks proxy lookups.
                    proxySelectorUris.add(uri);
                }
                return Collections.singletonList(Proxy.NO_PROXY);
            }

            @Override
            public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
                // no-op
            }
        });

        try {
            HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
            assertEquals("Target", readAscii(connection.getInputStream()));

            // Inspect the redirect request to see what request was actually made.
            RecordedRequest actualRequest = server2.takeRequest();
            assertEquals(targetPath + invalidSuffix, actualRequest.getPath());

            // The first URI will be the initial request. We want to inspect the redirect.
            URI uri = proxySelectorUris.get(1);
            // The proxy is selected by Address alone (not the whole target URI).
            // In OkHttp, HttpEngine.createAddress() converts to an Address and the
            // RouteSelector converts back to address.url().
            assertEquals(server2.getUrl("/").toString(), uri.toString());
        } finally {
            ProxySelector.setDefault(originalSelector);
            server2.shutdown();
        }
    }

    @Test public void instanceFollowsRedirects() throws Exception {
        testInstanceFollowsRedirects("http://www.google.com/");
        testInstanceFollowsRedirects("https://www.google.com/");
    }

    private void testInstanceFollowsRedirects(String spec) throws Exception {
        URL url = new URL(spec);
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.setInstanceFollowRedirects(true);
        assertTrue(urlConnection.getInstanceFollowRedirects());
        urlConnection.setInstanceFollowRedirects(false);
        assertFalse(urlConnection.getInstanceFollowRedirects());
    }

    @Test public void followRedirects() throws Exception {
        testFollowRedirects("http://www.google.com/");
        testFollowRedirects("https://www.google.com/");
    }

    private void testFollowRedirects(String spec) throws Exception {
        URL url = new URL(spec);
        boolean originalValue = HttpURLConnection.getFollowRedirects();
        try {
            HttpURLConnection.setFollowRedirects(false);
            assertFalse(HttpURLConnection.getFollowRedirects());

            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            assertFalse(connection.getInstanceFollowRedirects());

            HttpURLConnection.setFollowRedirects(true);
            assertTrue(HttpURLConnection.getFollowRedirects());

            HttpURLConnection connection2 = (HttpURLConnection) url.openConnection();
            assertTrue(connection2.getInstanceFollowRedirects());
        } finally {
            HttpURLConnection.setFollowRedirects(originalValue);
        }
    }

    @Test public void response300MultipleChoiceWithPost() throws Exception {
        // Chrome doesn't follow the redirect, but Firefox and the RI both do
        testResponseRedirectedWithPost(HttpURLConnection.HTTP_MULT_CHOICE);
    }

    @Test public void response301MovedPermanentlyWithPost() throws Exception {
        testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_PERM);
    }

    @Test public void response302MovedTemporarilyWithPost() throws Exception {
        testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_TEMP);
    }

    @Test public void response303SeeOtherWithPost() throws Exception {
        testResponseRedirectedWithPost(HttpURLConnection.HTTP_SEE_OTHER);
    }

    private void testResponseRedirectedWithPost(int redirectCode) throws Exception {
        server.enqueue(new MockResponse()
                .setResponseCode(redirectCode)
                .addHeader("Location: /page2")
                .setBody("This page has moved!"));
        server.enqueue(new MockResponse().setBody("Page 2"));
        server.play();

        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/page1").openConnection();
        connection.setDoOutput(true);
        byte[] requestBody = { 'A', 'B', 'C', 'D' };
        OutputStream outputStream = connection.getOutputStream();
        outputStream.write(requestBody);
        outputStream.close();
        assertEquals("Page 2", readAscii(connection.getInputStream()));
        assertTrue(connection.getDoOutput());

        RecordedRequest page1 = server.takeRequest();
        assertEquals("POST /page1 HTTP/1.1", page1.getRequestLine());
        assertEquals(Arrays.toString(requestBody), Arrays.toString(page1.getBody()));

        RecordedRequest page2 = server.takeRequest();
        assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine());
    }

    @Test public void response305UseProxy() throws Exception {
        server.play();
        server.enqueue(new MockResponse()
                .setResponseCode(HttpURLConnection.HTTP_USE_PROXY)
                .addHeader("Location: " + server.getUrl("/"))
                .setBody("This page has moved!"));
        server.enqueue(new MockResponse().setBody("Proxy Response"));

        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/foo").openConnection();
        // Fails on the RI, which gets "Proxy Response"
        assertEquals("This page has moved!", readAscii(connection.getInputStream()));

        RecordedRequest page1 = server.takeRequest();
        assertEquals("GET /foo HTTP/1.1", page1.getRequestLine());
        assertEquals(1, server.getRequestCount());
    }

    @Test public void httpsWithCustomTrustManager() throws Exception {
        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
        RecordingTrustManager trustManager = new RecordingTrustManager();
        SSLContext sc = SSLContext.getInstance("TLS");
        sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());

        HostnameVerifier defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
        HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
        SSLSocketFactory defaultSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        try {
            TestSSLContext testSSLContext = createDefaultTestSSLContext();
            server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
            server.enqueue(new MockResponse().setBody("ABC"));
            server.enqueue(new MockResponse().setBody("DEF"));
            server.enqueue(new MockResponse().setBody("GHI"));
            server.play();

            URL url = server.getUrl("/");
            assertEquals("ABC", readAscii(url.openStream()));
            assertEquals("DEF", readAscii(url.openStream()));
            assertEquals("GHI", readAscii(url.openStream()));

            assertEquals(Arrays.asList("verify " + hostName), hostnameVerifier.calls);
            assertEquals(Arrays.asList("checkServerTrusted ["
                    + "CN=Local Host 3, "
                    + "CN=Test Intermediate Certificate Authority 2, "
                    + "CN=Test Root Certificate Authority 1"
                    + "] ECDHE_RSA"),
                    trustManager.calls);
        } finally {
            HttpsURLConnection.setDefaultHostnameVerifier(defaultHostnameVerifier);
            HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory);
        }
    }

    @Test public void setSSLSocketFactory_null() throws Exception {
        URL url = new URL("https://google.com");
        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
        try {
            connection.setSSLSocketFactory(null);
            fail();
        } catch (IllegalArgumentException expected) {
        }
    }

    @Test public void setDefaultSSLSocketFactory_null() {
        try {
            HttpsURLConnection.setDefaultSSLSocketFactory(null);
            fail();
        } catch (IllegalArgumentException expected) {
        }
    }

    /**
     * Test that the timeout period is honored. The connect timeout is applied to each socket
     * connection attempt. If a hostname resolves to multiple IPs HttpURLConnection will wait the
     * full timeout for each.
     */
    @Test public void connectTimeouts() throws IOException {
        // During CTS tests we are limited in what host names we can depend on and unfortunately
        // DNS lookups are not pluggable through standard APIs. During manual testing you should be
        // able to change this to any name that can be resolved to multiple IPs and it should still
        // work.
        String hostName = "localhost";
        int expectedConnectionAttempts = InetAddress.getAllByName(hostName).length;
        int perSocketTimeout = 1000;
        final int[] socketCreationCount = new int[1];
        final int[] socketConnectTimeouts = new int[expectedConnectionAttempts];

        // It is difficult to force socket timeouts reliably so we replace the default SocketFactory
        // and have it create Sockets that always time out when connect(SocketAddress, int) is
        // called.
        SocketFactory originalDefault = SocketFactory.getDefault();
        try {
            // Override the default SocketFactory so we can intercept socket creation.
            SocketFactory.setDefault(new DelegatingSocketFactory(originalDefault) {
                @Override
                protected Socket configureSocket(Socket socket) throws IOException {
                    final int attemptNumber = socketCreationCount[0]++;
                    Socket socketWrapper = new DelegatingSocket(socket) {
                        @Override
                        public void connect(SocketAddress endpoint, int timeout)
                                throws IOException {
                            socketConnectTimeouts[attemptNumber] = timeout;
                            throw new SocketTimeoutException("Simulated timeout after " + timeout);
                        }
                    };
                    return socketWrapper;
                }
            });

            URLConnection urlConnection = new URL("http://" + hostName + "/").openConnection();
            urlConnection.setConnectTimeout(perSocketTimeout);
            urlConnection.getInputStream();
            fail();
        } catch (SocketTimeoutException e) {
            assertEquals(expectedConnectionAttempts, socketCreationCount[0]);
            for (int i = 0; i < expectedConnectionAttempts; i++) {
                assertEquals(perSocketTimeout, socketConnectTimeouts[i]);
            }
        } finally {
            SocketFactory.setDefault(originalDefault);
        }
    }

    @Test public void readTimeouts() throws IOException {
        /*
         * This relies on the fact that MockWebServer doesn't close the
         * connection after a response has been sent. This causes the client to
         * try to read more bytes than are sent, which results in a timeout.
         */
        MockResponse timeout = new MockResponse()
                .setBody("ABC")
                .clearHeaders()
                .addHeader("Content-Length: 4");
        server.enqueue(timeout);
        server.enqueue(new MockResponse().setBody("unused")); // to keep the server alive
        server.play();

        URLConnection urlConnection = server.getUrl("/").openConnection();
        urlConnection.setReadTimeout(1000);
        InputStream in = urlConnection.getInputStream();
        assertEquals('A', in.read());
        assertEquals('B', in.read());
        assertEquals('C', in.read());
        try {
            in.read(); // if Content-Length was accurate, this would return -1 immediately
            fail();
        } catch (SocketTimeoutException expected) {
        }
    }

    @Test public void setChunkedEncodingAsRequestProperty()
            throws IOException, InterruptedException {
        server.enqueue(new MockResponse());
        server.play();

        HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
        urlConnection.setRequestProperty("Transfer-encoding", "chunked");
        urlConnection.setDoOutput(true);
        urlConnection.getOutputStream().write("ABC".getBytes("UTF-8"));
        assertEquals(200, urlConnection.getResponseCode());

        RecordedRequest request = server.takeRequest();
        assertEquals("ABC", new String(request.getBody(), "UTF-8"));
    }

    @Test public void connectionCloseInRequest() throws IOException, InterruptedException {
        server.enqueue(new MockResponse()); // server doesn't honor the connection: close header!
        server.enqueue(new MockResponse());
        server.play();

        HttpURLConnection a = (HttpURLConnection) server.getUrl("/").openConnection();
        a.setRequestProperty("Connection", "close");
        assertEquals(200, a.getResponseCode());

        HttpURLConnection b = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals(200, b.getResponseCode());

        assertEquals(0, server.takeRequest().getSequenceNumber());
        assertEquals("When connection: close is used, each request should get its own connection",
                0, server.takeRequest().getSequenceNumber());
    }

    @Test public void connectionCloseInResponse() throws IOException, InterruptedException {
        server.enqueue(new MockResponse().addHeader("Connection: close"));
        server.enqueue(new MockResponse());
        server.play();

        HttpURLConnection a = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals(200, a.getResponseCode());

        HttpURLConnection b = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals(200, b.getResponseCode());

        assertEquals(0, server.takeRequest().getSequenceNumber());
        assertEquals("When connection: close is used, each request should get its own connection",
                0, server.takeRequest().getSequenceNumber());
    }

    @Test public void connectionCloseWithRedirect() throws IOException, InterruptedException {
        MockResponse response = new MockResponse()
                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
                .addHeader("Location: /foo")
                .addHeader("Connection: close");
        server.enqueue(response);
        server.enqueue(new MockResponse().setBody("This is the new location!"));
        server.play();

        URLConnection connection = server.getUrl("/").openConnection();
        assertEquals("This is the new location!", readAscii(connection.getInputStream()));

        assertEquals(0, server.takeRequest().getSequenceNumber());
        assertEquals("When connection: close is used, each request should get its own connection",
                0, server.takeRequest().getSequenceNumber());
    }

    @Test public void responseCodeDisagreesWithHeaders() throws IOException, InterruptedException {
        server.enqueue(new MockResponse()
                .setResponseCode(HttpURLConnection.HTTP_NO_CONTENT)
                .setBody("This body is not allowed!"));
        server.play();

        URLConnection connection = server.getUrl("/").openConnection();
        assertEquals("This body is not allowed!", readAscii(connection.getInputStream()));
    }

    @Test public void singleByteReadIsSigned() throws IOException {
        server.enqueue(new MockResponse().setBody(new byte[] { -2, -1 }));
        server.play();

        URLConnection connection = server.getUrl("/").openConnection();
        InputStream in = connection.getInputStream();
        assertEquals(254, in.read());
        assertEquals(255, in.read());
        assertEquals(-1, in.read());
    }

    @Test public void flushAfterStreamTransmittedWithChunkedEncoding() throws IOException {
        testFlushAfterStreamTransmitted(TransferKind.CHUNKED);
    }

    @Test public void flushAfterStreamTransmittedWithFixedLength() throws IOException {
        testFlushAfterStreamTransmitted(TransferKind.FIXED_LENGTH);
    }

    @Test public void flushAfterStreamTransmittedWithNoLengthHeaders() throws IOException {
        testFlushAfterStreamTransmitted(TransferKind.END_OF_STREAM);
    }

    /**
     * We explicitly permit apps to close the upload stream even after it has
     * been transmitted.  We also permit flush so that buffered streams can
     * do a no-op flush when they are closed. http://b/3038470
     */
    private void testFlushAfterStreamTransmitted(TransferKind transferKind) throws IOException {
        server.enqueue(new MockResponse().setBody("abc"));
        server.play();

        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        connection.setDoOutput(true);
        byte[] upload = "def".getBytes("UTF-8");

        if (transferKind == TransferKind.CHUNKED) {
            connection.setChunkedStreamingMode(0);
        } else if (transferKind == TransferKind.FIXED_LENGTH) {
            connection.setFixedLengthStreamingMode(upload.length);
        }

        OutputStream out = connection.getOutputStream();
        out.write(upload);
        assertEquals("abc", readAscii(connection.getInputStream()));

        out.flush(); // dubious but permitted
        try {
            out.write("ghi".getBytes("UTF-8"));
            fail();
        } catch (IOException expected) {
        }
    }

    @Test public void getHeadersThrows() throws IOException {
        server.enqueue(new MockResponse().setSocketPolicy(DISCONNECT_AT_START));
        server.play();

        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        try {
            connection.getInputStream();
            fail();
        } catch (IOException expected) {
        }

        try {
            connection.getInputStream();
            fail();
        } catch (IOException expected) {
        }
    }

    @Test public void readTimeoutsOnRecycledConnections() throws Exception {
        server.enqueue(new MockResponse().setBody("ABC"));
        server.play();

        // The request should work once and then fail
        URLConnection connection = server.getUrl("").openConnection();
        // Read timeout of a day, sure to cause the test to timeout and fail.
        connection.setReadTimeout(24 * 3600 * 1000);
        InputStream input = connection.getInputStream();
        assertEquals("ABC", readAscii(input));
        input.close();
        try {
            connection = server.getUrl("").openConnection();
            // Set the read timeout back to 100ms, this request will time out
            // because we've only enqueued one response.
            connection.setReadTimeout(100);
            connection.getInputStream();
            fail();
        } catch (IOException expected) {
        }
    }

    /**
     * This test goes through the exhaustive set of interesting ASCII characters
     * because most of those characters are interesting in some way according to
     * RFC 2396 and RFC 2732. http://b/1158780
     * After M, Android's HttpURLConnection started canonicalizing hostnames to lower case, IDN
     * encoding and being more strict about invalid characters.
     */
    @Test public void urlCharacterMapping() throws Exception {
        server.setDispatcher(new Dispatcher() {
            @Override public MockResponse dispatch(RecordedRequest request)
                throws InterruptedException {
                return new MockResponse();
            }
        });
        server.play();

        // alphanum
        testUrlToUriMapping("abzABZ09", "abzabz09", "abzABZ09", "abzABZ09", "abzABZ09");
        testUrlToRequestMapping("abzABZ09", "abzABZ09", "abzABZ09");

        // control characters

        // On JB-MR2 and below, we would allow a host containing \u0000
        // and then generate a request with a Host header that violated RFC2616.
        // We now reject such hosts.
        //
        // The ideal behaviour here is to be "lenient" about the host and rewrite
        // it, but attempting to do so introduces a new range of incompatible
        // behaviours.
        testUrlToUriMapping("\u0000", null, "%00", "%00", "%00"); // RI fails this
        testUrlToRequestMapping("\u0000", "%00", "%00");

        testUrlToUriMapping("\u0001", null, "%01", "%01", "%01");
        testUrlToRequestMapping("\u0001", "%01", "%01");

        testUrlToUriMapping("\u001f", null, "%1F", "%1F", "%1F");
        testUrlToRequestMapping("\u001f", "%1F", "%1F");

        // ascii characters
        testUrlToUriMapping("%20", null, "%20", "%20", "%20");
        testUrlToRequestMapping("%20", "%20", "%20");
        testUrlToUriMapping(" ", null, "%20", "%20", "%20");
        testUrlToRequestMapping(" ", "%20", "%20");
        testUrlToUriMapping("!", "!", "!", "!", "!");
        testUrlToRequestMapping("!", "!", "!");
        testUrlToUriMapping("\"", null, "%22", "%22", "%22");
        testUrlToRequestMapping("\"", "%22", "%22");
        testUrlToUriMapping("#", null, null, null, "%23");
        testUrlToRequestMapping("#", null, null);
        testUrlToUriMapping("$", "$", "$", "$", "$");
        testUrlToRequestMapping("$", "$", "$");
        testUrlToUriMapping("&", "&", "&", "&", "&");
        testUrlToRequestMapping("&", "&", "&");

        // http://b/30405333 - upstream OkHttp encodes single quote (') as %27 in query parameters
        // but this breaks iTunes remote apps: iTunes currently does not accept %27 so we have a
        // local patch to retain the historic Android behavior of not encoding single quote.
        testUrlToUriMapping("'", "'", "'", "'", "'");
        testUrlToRequestMapping("'", "'", "'");

        testUrlToUriMapping("(", "(", "(", "(", "(");
        testUrlToRequestMapping("(", "(", "(");
        testUrlToUriMapping(")", ")", ")", ")", ")");
        testUrlToRequestMapping(")", ")", ")");
        testUrlToUriMapping("*", "*", "*", "*", "*");
        testUrlToRequestMapping("*", "*", "*");
        testUrlToUriMapping("+", "+", "+", "+", "+");
        testUrlToRequestMapping("+", "+", "+");
        testUrlToUriMapping(",", ",", ",", ",", ",");
        testUrlToRequestMapping(",", ",", ",");
        testUrlToUriMapping("-", "-", "-", "-", "-");
        testUrlToRequestMapping("-", "-", "-");
        testUrlToUriMapping(".", null, ".", ".", ".");
        testUrlToRequestMapping(".", ".", ".");
        testUrlToUriMapping(".foo", ".foo", ".foo", ".foo", ".foo");
        testUrlToRequestMapping(".foo", ".foo", ".foo");
        testUrlToUriMapping("/", null, "/", "/", "/");
        testUrlToRequestMapping("/", "/", "/");
        testUrlToUriMapping(":", null, ":", ":", ":");
        testUrlToRequestMapping(":", ":", ":");
        testUrlToUriMapping(";", ";", ";", ";", ";");
        testUrlToRequestMapping(";", ";", ";");
        testUrlToUriMapping("<", null, "%3C", "%3C", "%3C");
        testUrlToRequestMapping("<", "%3C", "%3C");
        testUrlToUriMapping("=", "=", "=", "=", "=");
        testUrlToRequestMapping("=", "=", "=");
        testUrlToUriMapping(">", null, "%3E", "%3E", "%3E");
        testUrlToRequestMapping(">", "%3E", "%3E");
        testUrlToUriMapping("?", null, null, "?", "?");
        testUrlToRequestMapping("?", null, "?");
        testUrlToUriMapping("@", null, "@", "@", "@");
        testUrlToRequestMapping("@", "@", "@");
        testUrlToUriMapping("[", null, null, null, "%5B");
        testUrlToRequestMapping("[", null, null);
        testUrlToUriMapping("\\", null, null, null, "%5C");
        testUrlToRequestMapping("\\", null, null);
        testUrlToUriMapping("]", null, null, null, "%5D");
        testUrlToRequestMapping("]", null, null);
        testUrlToUriMapping("^", null, "%5E", null, "%5E");
        testUrlToRequestMapping("^", "%5E", null);
        testUrlToUriMapping("_", "_", "_", "_", "_");
        testUrlToRequestMapping("_", "_", "_");
        testUrlToUriMapping("`", null, "%60", null, "%60");
        testUrlToRequestMapping("`", "%60", null);
        testUrlToUriMapping("{", null, "%7B", null, "%7B");
        testUrlToRequestMapping("{", "%7B", null);
        testUrlToUriMapping("|", null, "%7C", null, "%7C");
        testUrlToRequestMapping("|", "%7C", null);
        testUrlToUriMapping("}", null, "%7D", null, "%7D");
        testUrlToRequestMapping("}", "%7D", null);
        testUrlToUriMapping("~", "~", "~", "~", "~");
        testUrlToRequestMapping("~", "~", "~");
        testUrlToUriMapping("\u007f", null, "%7F", "%7F", "%7F");
        testUrlToRequestMapping("\u007f", "%7F", "%7F");

        // beyond ASCII

        // 0x80 is the code point for the Euro sign in CP1252 (but not 8859-15 or Unicode).
        // Unicode code point 0x80 is a control character and maps to {0xC2, 0x80} in UTF-8.
        // 0x80 is outside of the ASCII range and is not supported by IDN in hostnames.
        testUrlToUriMapping("\u0080", null, "%C2%80", "%C2%80", "%C2%80");
        testUrlToRequestMapping("\u0080", "%C2%80", "%C2%80");

        // More complicated transformations for the authorities below.

        // 0x20AC is the code point for the Euro sign in Unicode.
        // Unicode code point 0x20AC maps to {0xE2, 0x82, 0xAC} in UTF-8
        // 0x20AC is not supported by all registrars but there are some legacy domains that
        // use it and Android currently supports IDN conversion for it.
        testUrlToUriMapping("\u20ac", null /* skip */, "%E2%82%AC", "%E2%82%AC", "%E2%82%AC");
        testUrlToUriMappingAuthority("http://host\u20ac.tld/", "http://xn--host-yv7a.tld/");
        testUrlToRequestMapping("\u20ac",  "%E2%82%AC", "%E2%82%AC");

        // UTF-16 {0xD842, 0xDF9F} -> Unicode 0x20B9F (a Kanji character)
        // Unicode code point 0x20B9F maps to {0xF0, 0xA0, 0xAE, 0x9F} in UTF-8
        // IDN can deal with this code point.
        testUrlToUriMapping("\ud842\udf9f", null /* skip */, "%F0%A0%AE%9F", "%F0%A0%AE%9F",
            "%F0%A0%AE%9F");
        testUrlToUriMappingAuthority("http://host\uD842\uDF9F.tld/", "http://xn--host-ov06c.tld/");
        testUrlToRequestMapping("\ud842\udf9f",  "%F0%A0%AE%9F", "%F0%A0%AE%9F");
    }

    private void testUrlToUriMappingAuthority(String urlString, String expectedUriString)
        throws Exception {
        URI authorityUri = backdoorUrlToUri(new URL(urlString));
        assertEquals(expectedUriString, authorityUri.toString());
    }

    /**
     * Exercises HttpURLConnection to convert URL to a URI. Unlike URL#toURI,
     * HttpURLConnection recovers from URLs with unescaped but unsupported URI
     * characters like '{' and '|' by escaping these characters.
     */
    private URI backdoorUrlToUri(URL url) throws Exception {
        final AtomicReference<URI> uriReference = new AtomicReference<URI>();

        ResponseCache.setDefault(new ResponseCache() {
            @Override public CacheRequest put(URI uri, URLConnection connection)
                    throws IOException {
                return null;
            }
            @Override public CacheResponse get(URI uri, String requestMethod,
                    Map<String, List<String>> requestHeaders) throws IOException {
                uriReference.set(uri);
                throw new UnsupportedOperationException();
            }
        });

        try {
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.getResponseCode();
        } catch (Exception expected) {
        }

        return uriReference.get();
    }

    /*
     * Test the request that would be made by making an actual request a MockWebServer and capturing
     * the request made.
     *
     * Any "as" values that are null are not tested.
     */
    private void testUrlToRequestMapping(
            String string, String asFile, String asQuery) throws Exception {
        if (asFile != null) {
            URL fileUrl = server.getUrl("/file" + string + "/#discarded");
            HttpURLConnection urlConnection = (HttpURLConnection) fileUrl.openConnection();
            // Bypass the cache.
            urlConnection.setUseCaches(false);

            assertEquals(200, urlConnection.getResponseCode());
            assertEquals("/file" + asFile + "/", server.takeRequest().getPath());
        }
        if (asQuery != null) {
            URL queryUrl = server.getUrl("/file?q" + string + "=x#discarded");
            HttpURLConnection urlConnection = (HttpURLConnection) queryUrl.openConnection();
            // Bypass the cache.
            urlConnection.setUseCaches(false);

            assertEquals(200, urlConnection.getResponseCode());
            assertEquals("/file?q" + asQuery + "=x", server.takeRequest().getPath());
        }
    }

    /*
     * Test the request that would be made by looking at the URI presented to the cache. This
     * includes the likely host name that would be used if a request were made. The cache throws an
     * exception so no request is actually made.
     *
     * Any "as" values that are null are not tested.
     */
    private void testUrlToUriMapping(String string, String asAuthority, String asFile,
            String asQuery, String asFragment) throws Exception {
        if (asAuthority != null) {
            URI authorityUri = backdoorUrlToUri(new URL("http://host" + string + ".tld/"));
            assertEquals("http://host" + asAuthority + ".tld/", authorityUri.toString());
        }
        if (asFile != null) {
            URI fileUri = backdoorUrlToUri(new URL("http://host.tld/file" + string + "/"));
            assertEquals("http://host.tld/file" + asFile + "/", fileUri.toString());
        }
        if (asQuery != null) {
            URI queryUri = backdoorUrlToUri(new URL("http://host.tld/file?q" + string + "=x"));
            assertEquals("http://host.tld/file?q" + asQuery + "=x", queryUri.toString());
        }
        assertEquals("http://host.tld/file#" + asFragment + "-x",
            backdoorUrlToUri(new URL("http://host.tld/file#" + asFragment + "-x")).toString());
    }

    @Test public void hostWithNul() throws Exception {
        URL url = new URL("http://host\u0000/");
        try {
            url.openStream();
            fail();
        } catch (UnknownHostException expected) {}
    }

    /**
     * Don't explode if the cache returns a null body. http://b/3373699
     */
    @Test public void responseCacheReturnsNullOutputStream() throws Exception {
        final AtomicBoolean aborted = new AtomicBoolean();
        ResponseCache.setDefault(new ResponseCache() {
            @Override public CacheResponse get(URI uri, String requestMethod,
                    Map<String, List<String>> requestHeaders) throws IOException {
                return null;
            }
            @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
                return new CacheRequest() {
                    @Override public void abort() {
                        aborted.set(true);
                    }
                    @Override public OutputStream getBody() throws IOException {
                        return null;
                    }
                };
            }
        });

        server.enqueue(new MockResponse().setBody("abcdef"));
        server.play();

        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        InputStream in = connection.getInputStream();
        assertEquals("abc", readAscii(in, 3));
        in.close();
        assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here
    }


    /**
     * http://code.google.com/p/android/issues/detail?id=14562
     */
    @Test public void readAfterLastByte() throws Exception {
        server.enqueue(new MockResponse()
                .setBody("ABC")
                .clearHeaders()
                .addHeader("Connection: close")
                .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
        server.play();

        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        InputStream in = connection.getInputStream();
        assertEquals("ABC", readAscii(in, 3));
        assertEquals(-1, in.read());
        assertEquals(-1, in.read()); // throws IOException in Gingerbread
    }

    @Test public void getContent() throws Exception {
        server.enqueue(new MockResponse().setBody("A"));
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        InputStream in = (InputStream) connection.getContent();
        assertEquals("A", readAscii(in));
    }

    @Test public void getContentOfType() throws Exception {
        server.enqueue(new MockResponse().setBody("A"));
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        try {
            connection.getContent(null);
            fail();
        } catch (NullPointerException expected) {
        }
        try {
            connection.getContent(new Class[] { null });
            fail();
        } catch (NullPointerException expected) {
        }
        assertNull(connection.getContent(new Class[] { getClass() }));
        connection.disconnect();
    }

    @Test public void getOutputStreamOnGetFails() throws Exception {
        server.enqueue(new MockResponse());
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        try {
            connection.getOutputStream();
            fail();
        } catch (ProtocolException expected) {
        }
    }

    @Test public void getOutputAfterGetInputStreamFails() throws Exception {
        server.enqueue(new MockResponse());
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        connection.setDoOutput(true);
        try {
            connection.getInputStream();
            connection.getOutputStream();
            fail();
        } catch (ProtocolException expected) {
        }
    }

    @Test public void setDoOutputOrDoInputAfterConnectFails() throws Exception {
        server.enqueue(new MockResponse());
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        connection.connect();
        try {
            connection.setDoOutput(true);
            fail();
        } catch (IllegalStateException expected) {
        }
        try {
            connection.setDoInput(true);
            fail();
        } catch (IllegalStateException expected) {
        }
        connection.disconnect();
    }

    @Test public void lastModified() throws Exception {
        server.enqueue(new MockResponse()
                .addHeader("Last-Modified", "Wed, 27 Nov 2013 11:26:00 GMT")
                .setBody("Hello"));
        server.play();

        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        connection.connect();

        assertEquals(1385551560000L, connection.getLastModified());
        assertEquals(1385551560000L, connection.getHeaderFieldDate("Last-Modified", -1));
    }

    @Test public void clientSendsContentLength() throws Exception {
        server.enqueue(new MockResponse().setBody("A"));
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        connection.setDoOutput(true);
        OutputStream out = connection.getOutputStream();
        out.write(new byte[] { 'A', 'B', 'C' });
        out.close();
        assertEquals("A", readAscii(connection.getInputStream()));
        RecordedRequest request = server.takeRequest();
        assertContains(request.getHeaders(), "Content-Length: 3");
    }

    @Test public void getContentLengthConnects() throws Exception {
        server.enqueue(new MockResponse().setBody("ABC"));
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals(3, connection.getContentLength());
        connection.disconnect();
    }

    @Test public void getContentTypeConnects() throws Exception {
        server.enqueue(new MockResponse()
                .addHeader("Content-Type: text/plain")
                .setBody("ABC"));
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals("text/plain", connection.getContentType());
        connection.disconnect();
    }

    @Test public void getContentEncodingConnects() throws Exception {
        server.enqueue(new MockResponse()
                .addHeader("Content-Encoding: identity")
                .setBody("ABC"));
        server.play();
        HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
        assertEquals("identity", connection.getContentEncoding());
        connection.disconnect();
    }

    @Test public void setContentHandlerFactory() throws Exception {
        // Verify that the static ContentHandlerFactory is null
        Field f = URLConnection.class.getDeclaredField("factory");
        f.setAccessible(true);
        assertNull(f.get(null));

        try {
            URLConnection.setContentHandlerFactory(new MockContentHandlerFactory());
            String msg = "ABC";
            server.enqueue(new MockResponse()
                    .addHeader("Content-Type: text/plain")
                    .setBody(msg));
            server.enqueue(new MockResponse()
                    .addHeader("Content-Type: " + MockContentHandlerFactory.HANDLED_MIME_TYPE)
                    .setBody(msg));
            server.play();

            HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
            assertEquals("text/plain", connection.getContentType());
            assertEquals(msg, readAscii((InputStream) connection.getContent()));

            connection = (HttpURLConnection) server.getUrl("/").openConnection();
            assertEquals(MockContentHandlerFactory.HANDLED_MIME_TYPE, connection.getContentType());
            assertEquals(MockContentHandler.CONTENT, connection.getContent());
        } finally {
            // reset the static ContentHandlerFactory
            f.set(null, null);
        }
    }

    private static class MockContentHandler extends ContentHandler {

        private static final String CONTENT = "SECRET_CONTENT";

        @Override
        public Object getContent(URLConnection urlc) throws IOException {
            return CONTENT;
        }
    }

    private static class MockContentHandlerFactory implements ContentHandlerFactory {

        private static final String HANDLED_MIME_TYPE = "text/secret";

        @Override
        public ContentHandler createContentHandler(String mimetype) {
            if (HANDLED_MIME_TYPE.equals(mimetype)) {
                return new MockContentHandler();
            }
            return null;
        }
    }

    // http://b/4361656
    @Test public void urlContainsQueryButNoPath() throws Exception {
        server.enqueue(new MockResponse().setBody("A"));
        server.play();
        URL url = new URL("http", server.getHostName(), server.getPort(), "?query");
        assertEquals("A", readAscii(url.openConnection().getInputStream()));
        RecordedRequest request = server.takeRequest();
        assertEquals("GET /?query HTTP/1.1", request.getRequestLine());
    }

    // http://code.google.com/p/android/issues/detail?id=20442
    @Test public void inputStreamAvailableWithChunkedEncoding() throws Exception {
        checkInputStreamAvailable(TransferKind.CHUNKED);
    }

    @Test public void inputStreamAvailableWithContentLengthHeader() throws Exception {
        checkInputStreamAvailable(TransferKind.FIXED_LENGTH);
    }

    @Test public void inputStreamAvailableWithNoLengthHeaders() throws Exception {
        checkInputStreamAvailable(TransferKind.END_OF_STREAM);
    }

    private void checkInputStreamAvailable(TransferKind transferKind) throws IOException {
        String body = "ABCDEFGH";
        MockResponse response = new MockResponse();
        transferKind.setBody(response, body, 4);
        server.enqueue(response);
        server.play();
        URLConnection connection = server.getUrl("/").openConnection();
        InputStream in = connection.getInputStream();
        for (int i = 0; i < body.length(); i++) {
            assertTrue(in.available() >= 0);
            assertEquals(body.charAt(i), in.read());
        }
        assertEquals(0, in.available());
        assertEquals(-1, in.read());
    }

    // http://code.google.com/p/android/issues/detail?id=28095
    @Test public void invalidIpv4Address() throws Exception {
        try {
            URI uri = new URI("http://1111.111.111.111/index.html");
            uri.toURL().openConnection().connect();
            fail();
        } catch (UnknownHostException expected) {
        }
    }

    @Test public void connectIpv6() throws Exception {
        server.enqueue(new MockResponse().setBody("testConnectIpv6 body"));
        server.play();
        URL url = new URL("http://[::1]:" + server.getPort() + "/");
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        assertContent("testConnectIpv6 body", connection);
    }

    // http://code.google.com/p/android/issues/detail?id=16895
    @Test public void urlWithSpaceInHost() throws Exception {
        URLConnection urlConnection = new URL("http://and roid.com/").openConnection();
        try {
            urlConnection.getInputStream();
            fail();
        } catch (UnknownHostException expected) {
        }
    }

    // http://code.google.com/p/android/issues/detail?id=16895
    @Test public void urlWithSpaceInHostViaHttpProxy() throws Exception {
        server.enqueue(new MockResponse());
        server.play();
        URLConnection urlConnection = new URL("http://and roid.com/")
                .openConnection(server.toProxyAddress());

        try {
            // This test is to check that a NullPointerException is not thrown.
            urlConnection.getInputStream();
            fail(); // the RI makes a bogus proxy request for "GET http://and roid.com/ HTTP/1.1"
        } catch (UnknownHostException expected) {
        }
    }

    /** Checks that if the first TLS handshake fails, no fallback is attempted. */
    private void checkNoFallbackOnFailedHandshake(SSLSocketFactory clientSocketFactory,
            SSLSocketFactory serverSocketFactory,
            String... expectedProtocols)
            throws Exception {
        server.useHttps(serverSocketFactory, false);
        server.enqueue(new MockResponse().setSocketPolicy(FAIL_HANDSHAKE));
        server.enqueue(new MockResponse().setBody("This required fallbacks"));
        server.play();

        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
        // Keeps track of the client sockets created so that we can interrogate them.
        final boolean disableFallbackScsv = true;
        FallbackTestClientSocketFactory fallbackTestClientSocketFactory =
                new FallbackTestClientSocketFactory(clientSocketFactory, disableFallbackScsv);
        connection.setSSLSocketFactory(fallbackTestClientSocketFactory);
        try {
            connection.getInputStream().read();
            fail();
        } catch (SSLHandshakeException expected) {
        }
        List<SSLSocket> createdSockets = fallbackTestClientSocketFactory.getCreatedSockets();
        assertEquals(1, createdSockets.size());
        assertSslSocket((TlsFallbackDisabledScsvSSLSocket) createdSockets.get(0),
                false /* expectedWasFallbackScsvSet */, expectedProtocols);
    }

    @Test public void noSslFallback_specifiedProtocols() throws Exception {
        String[] enabledProtocols = { "TLSv1.2", "TLSv1.1" };
        TestSSLContext testSSLContext = createDefaultTestSSLContext();
        SSLSocketFactory serverSocketFactory =
                new LimitedProtocolsSocketFactory(
                        testSSLContext.serverContext.getSocketFactory(),
                        enabledProtocols);
        SSLSocketFactory clientSocketFactory = new LimitedProtocolsSocketFactory(
                testSSLContext.clientContext.getSocketFactory(), enabledProtocols);
        checkNoFallbackOnFailedHandshake(clientSocketFactory, serverSocketFactory,
                enabledProtocols);
    }

    @Test public void noSslFallback_defaultProtocols() throws Exception {
        // Will need to be updated if the enabled protocols in Android's SSLSocketFactory change
        String[] expectedEnabledProtocols = { "TLSv1.2", "TLSv1.1", "TLSv1" };

        TestSSLContext testSSLContext = createDefaultTestSSLContext();
        SSLSocketFactory serverSocketFactory = testSSLContext.serverContext.getSocketFactory();
        SSLSocketFactory clientSocketFactory = testSSLContext.clientContext.getSocketFactory();
        checkNoFallbackOnFailedHandshake(clientSocketFactory, serverSocketFactory,
                expectedEnabledProtocols);
    }

    private static void assertSslSocket(TlsFallbackDisabledScsvSSLSocket socket,
            boolean expectedWasFallbackScsvSet, String... expectedEnabledProtocols) {
        Set<String> enabledProtocols =
                new HashSet<String>(Arrays.asList(socket.getEnabledProtocols()));
        Set<String> expectedProtocolsSet = new HashSet<String>(Arrays.asList(expectedEnabledProtocols));
        assertEquals(expectedProtocolsSet, enabledProtocols);
        assertEquals(expectedWasFallbackScsvSet, socket.wasTlsFallbackScsvSet());
    }

    @Test public void inspectSslBeforeConnect() throws Exception {
        TestSSLContext testSSLContext = createDefaultTestSSLContext();
        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
        server.enqueue(new MockResponse());
        server.play();

        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
        assertNotNull(connection.getHostnameVerifier());
        try {
            connection.getLocalCertificates();
            fail();
        } catch (IllegalStateException expected) {
        }
        try {
            connection.getServerCertificates();
            fail();
        } catch (IllegalStateException expected) {
        }
        try {
            connection.getCipherSuite();
            fail();
        } catch (IllegalStateException expected) {
        }
        try {
            connection.getPeerPrincipal();
            fail();
        } catch (IllegalStateException expected) {
        }
    }

    /**
     * Test that we can inspect the SSL session after connect().
     * http://code.google.com/p/android/issues/detail?id=24431
     */
    @Test public void inspectSslAfterConnect() throws Exception {
        TestSSLContext testSSLContext = createDefaultTestSSLContext();
        server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
        server.enqueue(new MockResponse());
        server.play();

        HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
        connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
        connection.connect();
        try {
            assertNotNull(connection.getHostnameVerifier());
            assertNull(connection.getLocalCertificates());
            assertNotNull(connection.getServerCertificates());
            assertNotNull(connection.getCipherSuite());
            assertNotNull(connection.getPeerPrincipal());
        } finally {
            connection.disconnect();
        }
    }

    /**
     * Checks that OkHttp's certificate pinning logic is not used for the common case
     * of HttpsUrlConnections.
     *
     * <p>OkHttp 2.7 introduced logic for Certificate Pinning. We deliberately don't
     * expose any API surface that would interact with OkHttp's implementation because
     * Android has its own API / implementation for certificate pinning. We can't
     * easily test that there is *no* code path that would invoke OkHttp's certificate
     * pinning logic, so this test only covers the *common* code path of a
     * HttpsURLConnection as a confidence check.
     *
     * <p>To check whether OkHttp performs certificate pinning under the hood, this
     * test disables two {@link Platform} methods. In OkHttp 2.7.5, these two methods
     * are exclusively used in relation to certificate pinning. Android only provides
     * the minimal implementation of these methods to get OkHttp's tests to pass, so
     * they should never be invoked outside of OkHttp's tests.
     */
    @Test public void trustManagerAndTrustRootIndex_unusedForHttpsConnection() throws Exception {
        Platform platform = Platform.getAndSetForTest(new PlatformWithoutTrustManager());
        try {
            checkConnectViaHttps();
        } finally {
            Platform.getAndSetForTest(platform);
        }
    }

    /**
     * Similar to {@link #testTrustManagerAndTrustRootIndex_unusedForHttpsConnection()},
     * but for the HTTP case. In the HTTP case, no certificate or trust management
     * related logic should ever be involved at all, so some pretty basic things must
     * be going wrong in order for this test to (unexpectedly) invoke the corresponding
     * Platform methods.
     */
    @Test public void trustManagerAndTrustRootIndex_unusedForHttpConnection() throws Exception {
        Platform platform = Platform.getAndSetForTest(new PlatformWithoutTrustManager());
        try {
            server.enqueue(new MockResponse().setBody("response").setResponseCode(200));
            server.play();
            HttpURLConnection urlConnection =
                    (HttpURLConnection) server.getUrl("/").openConnection();
            assertEquals(200, urlConnection.getResponseCode());
        } finally {
            Platform.getAndSetForTest(platform);
        }
    }

    /**
     * A {@link Platform} that doesn't support two methods that, in OkHttp 2.7.5,
     * are exclusively used to provide custom CertificatePinning.
     */
    static class PlatformWithoutTrustManager extends Platform {
        @Override public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) {
            throw new AssertionError("Unexpected call");
        }
        @Override public TrustRootIndex trustRootIndex(X509TrustManager trustManager) {
            throw new AssertionError("Unexpected call");
        }
    }

    /**
     * Returns a gzipped copy of {@code bytes}.
     */
    public byte[] gzip(byte[] bytes) throws IOException {
        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
        OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
        gzippedOut.write(bytes);
        gzippedOut.close();
        return bytesOut.toByteArray();
    }

    /**
     * Reads at most {@code limit} characters from {@code in} and asserts that
     * content equals {@code expected}.
     */
    private void assertContent(String expected, URLConnection connection, int limit)
            throws IOException {
        connection.connect();
        assertEquals(expected, readAscii(connection.getInputStream(), limit));
        ((HttpURLConnection) connection).disconnect();
    }

    private void assertContent(String expected, URLConnection connection) throws IOException {
        assertContent(expected, connection, Integer.MAX_VALUE);
    }

    private static void assertHeaderPresent(RecordedRequest request, String headerName) {
        assertNotNull(headerName + " missing: " + request.getHeaders(),
                request.getHeader(headerName));
    }

    private void assertContains(List<String> list, String value) {
        assertTrue(list.toString(), list.contains(value));
    }

    private void assertContainsNoneMatching(List<String> list, String pattern) {
        for (String header : list) {
            if (header.matches(pattern)) {
                fail("Header " + header + " matches " + pattern);
            }
        }
    }

    private Set<String> newSet(String... elements) {
        return new HashSet<String>(Arrays.asList(elements));
    }

    private TestSSLContext createDefaultTestSSLContext() {
        TestSSLContext result = TestSSLContext.create();
        testSSLContextsToClose.add(result);
        return result;
    }

    enum TransferKind {
        CHUNKED() {
            @Override void setBody(MockResponse response, byte[] content, int chunkSize)
                    throws IOException {
                response.setChunkedBody(content, chunkSize);
            }
        },
        FIXED_LENGTH() {
            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
                response.setBody(content);
            }
        },
        END_OF_STREAM() {
            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
                response.setBody(content);
                response.setSocketPolicy(DISCONNECT_AT_END);
                for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
                    if (h.next().startsWith("Content-Length:")) {
                        h.remove();
                        break;
                    }
                }
            }
        };

        abstract void setBody(MockResponse response, byte[] content, int chunkSize)
                throws IOException;

        void setBody(MockResponse response, String content, int chunkSize) throws IOException {
            setBody(response, content.getBytes("UTF-8"), chunkSize);
        }
    }

    enum ProxyConfig {
        NO_PROXY() {
            @Override public HttpURLConnection connect(MockWebServer server, URL url)
                    throws IOException {
                return (HttpURLConnection) url.openConnection(Proxy.NO_PROXY);
            }
        },

        CREATE_ARG() {
            @Override public HttpURLConnection connect(MockWebServer server, URL url)
                    throws IOException {
                return (HttpURLConnection) url.openConnection(server.toProxyAddress());
            }
        },

        PROXY_SYSTEM_PROPERTY() {
            @Override public HttpURLConnection connect(MockWebServer server, URL url)
                    throws IOException {
                System.setProperty("proxyHost", "localhost");
                System.setProperty("proxyPort", Integer.toString(server.getPort()));
                return (HttpURLConnection) url.openConnection();
            }
        },

        HTTP_PROXY_SYSTEM_PROPERTY() {
            @Override public HttpURLConnection connect(MockWebServer server, URL url)
                    throws IOException {
                System.setProperty("http.proxyHost", "localhost");
                System.setProperty("http.proxyPort", Integer.toString(server.getPort()));
                return (HttpURLConnection) url.openConnection();
            }
        },

        HTTPS_PROXY_SYSTEM_PROPERTY() {
            @Override public HttpURLConnection connect(MockWebServer server, URL url)
                    throws IOException {
                System.setProperty("https.proxyHost", "localhost");
                System.setProperty("https.proxyPort", Integer.toString(server.getPort()));
                return (HttpURLConnection) url.openConnection();
            }
        };

        public abstract HttpURLConnection connect(MockWebServer server, URL url) throws IOException;
    }

    private static class RecordingTrustManager implements X509TrustManager {
        private final List<String> calls = new ArrayList<String>();

        public X509Certificate[] getAcceptedIssuers() {
            calls.add("getAcceptedIssuers");
            return new X509Certificate[] {};
        }

        public void checkClientTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            calls.add("checkClientTrusted " + certificatesToString(chain) + " " + authType);
        }

        public void checkServerTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            calls.add("checkServerTrusted " + certificatesToString(chain) + " " + authType);
        }

        private String certificatesToString(X509Certificate[] certificates) {
            List<String> result = new ArrayList<String>();
            for (X509Certificate certificate : certificates) {
                result.add(certificate.getSubjectDN() + " " + certificate.getSerialNumber());
            }
            return result.toString();
        }
    }

    private static class RecordingHostnameVerifier implements HostnameVerifier {
        private final List<String> calls = new ArrayList<String>();

        public boolean verify(String hostname, SSLSession session) {
            calls.add("verify " + hostname);
            return true;
        }
    }

    private static class SimpleAuthenticator extends Authenticator {
        /** base64("username:password") */
        private static final String BASE_64_CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ=";

        private String expectedPrompt;
        private RequestorType requestorType;
        private int requestingPort;
        private InetAddress requestingSite;
        private String requestingPrompt;
        private String requestingProtocol;
        private String requestingScheme;

        protected PasswordAuthentication getPasswordAuthentication() {
            requestorType = getRequestorType();
            requestingPort = getRequestingPort();
            requestingSite = getRequestingSite();
            requestingPrompt = getRequestingPrompt();
            requestingProtocol = getRequestingProtocol();
            requestingScheme = getRequestingScheme();
            return (expectedPrompt == null || expectedPrompt.equals(requestingPrompt))
                    ? new PasswordAuthentication("username", "password".toCharArray())
                    : null;
        }
    }

    /**
     * An SSLSocketFactory that delegates all calls.
     */
    private static class DelegatingSSLSocketFactory extends SSLSocketFactory {

        protected final SSLSocketFactory delegate;

        public DelegatingSSLSocketFactory(SSLSocketFactory delegate) {
            this.delegate = delegate;
        }

        @Override
        public String[] getDefaultCipherSuites() {
            return delegate.getDefaultCipherSuites();
        }

        @Override
        public String[] getSupportedCipherSuites() {
            return delegate.getSupportedCipherSuites();
        }

        @Override
        public SSLSocket createSocket(Socket s, String host, int port, boolean autoClose)
                throws IOException {
            return (SSLSocket) delegate.createSocket(s, host, port, autoClose);
        }

        @Override
        public SSLSocket createSocket() throws IOException {
            return (SSLSocket) delegate.createSocket();
        }

        @Override
        public SSLSocket createSocket(String host, int port)
                throws IOException, UnknownHostException {
            return (SSLSocket) delegate.createSocket(host, port);
        }

        @Override
        public SSLSocket createSocket(String host, int port, InetAddress localHost,
                int localPort) throws IOException, UnknownHostException {
            return (SSLSocket) delegate.createSocket(host, port, localHost, localPort);
        }

        @Override
        public SSLSocket createSocket(InetAddress host, int port) throws IOException {
            return (SSLSocket) delegate.createSocket(host, port);
        }

        @Override
        public SSLSocket createSocket(InetAddress address, int port,
                InetAddress localAddress, int localPort) throws IOException {
            return (SSLSocket) delegate.createSocket(address, port, localAddress, localPort);
        }

    }

    /**
     * An SSLSocketFactory that delegates calls but limits the enabled protocols for any created
     * sockets.
     */
    private static class LimitedProtocolsSocketFactory extends DelegatingSSLSocketFactory {

        private final String[] protocols;

        private LimitedProtocolsSocketFactory(SSLSocketFactory delegate, String... protocols) {
            super(delegate);
            this.protocols = protocols;
        }

        @Override
        public SSLSocket createSocket(Socket s, String host, int port, boolean autoClose)
                throws IOException {
            SSLSocket socket = (SSLSocket) delegate.createSocket(s, host, port, autoClose);
            socket.setEnabledProtocols(protocols);
            return socket;
        }

        @Override
        public SSLSocket createSocket() throws IOException {
            SSLSocket socket = (SSLSocket) delegate.createSocket();
            socket.setEnabledProtocols(protocols);
            return socket;
        }

        @Override
        public SSLSocket createSocket(String host, int port)
                throws IOException, UnknownHostException {
            SSLSocket socket = (SSLSocket) delegate.createSocket(host, port);
            socket.setEnabledProtocols(protocols);
            return socket;
        }

        @Override
        public SSLSocket createSocket(String host, int port, InetAddress localHost,
                int localPort) throws IOException, UnknownHostException {
            SSLSocket socket = (SSLSocket) delegate.createSocket(host, port, localHost, localPort);
            socket.setEnabledProtocols(protocols);
            return socket;
        }

        @Override
        public SSLSocket createSocket(InetAddress host, int port) throws IOException {
            SSLSocket socket = (SSLSocket) delegate.createSocket(host, port);
            socket.setEnabledProtocols(protocols);
            return socket;
        }

        @Override
        public SSLSocket createSocket(InetAddress address, int port,
                InetAddress localAddress, int localPort) throws IOException {
            SSLSocket socket =
                    (SSLSocket) delegate.createSocket(address, port, localAddress, localPort);
            socket.setEnabledProtocols(protocols);
            return socket;
        }
    }

    /**
     * A Socket that forwards all calls to public or protected methods, except for those
     * that Socket inherits from Object, to a delegate.
     */
    private static abstract class DelegatingSocket extends Socket {
        private final Socket delegate;

        public DelegatingSocket(Socket delegate) {
            if (delegate == null) {
                throw new NullPointerException();
            }
            this.delegate = delegate;
        }

        @Override public void bind(SocketAddress bindpoint) throws IOException { delegate.bind(bindpoint); }
        @Override public void close() throws IOException { delegate.close(); }
        @Override public void connect(SocketAddress endpoint) throws IOException { delegate.connect(endpoint); }
        @Override public void connect(SocketAddress endpoint, int timeout) throws IOException { delegate.connect(endpoint, timeout); }
        @Override public SocketChannel getChannel() { return delegate.getChannel(); }
        @Override public FileDescriptor getFileDescriptor$() { return delegate.getFileDescriptor$(); }
        @Override public InetAddress getInetAddress() { return delegate.getInetAddress(); }
        @Override public InputStream getInputStream() throws IOException { return delegate.getInputStream(); }
        @Override public boolean getKeepAlive() throws SocketException { return delegate.getKeepAlive(); }
        @Override public InetAddress getLocalAddress() { return delegate.getLocalAddress(); }
        @Override public int getLocalPort() { return delegate.getLocalPort(); }
        @Override public SocketAddress getLocalSocketAddress() { return delegate.getLocalSocketAddress(); }
        @Override public boolean getOOBInline() throws SocketException { return delegate.getOOBInline(); }
        @Override public OutputStream getOutputStream() throws IOException { return delegate.getOutputStream(); }
        @Override public int getPort() { return delegate.getPort(); }
        @Override public int getReceiveBufferSize() throws SocketException { return delegate.getReceiveBufferSize(); }
        @Override public SocketAddress getRemoteSocketAddress() { return delegate.getRemoteSocketAddress(); }
        @Override public boolean getReuseAddress() throws SocketException { return delegate.getReuseAddress(); }
        @Override public int getSendBufferSize() throws SocketException { return delegate.getSendBufferSize(); }
        @Override public int getSoLinger() throws SocketException { return delegate.getSoLinger(); }
        @Override public int getSoTimeout() throws SocketException { return delegate.getSoTimeout(); }
        @Override public boolean getTcpNoDelay() throws SocketException { return delegate.getTcpNoDelay(); }
        @Override public int getTrafficClass() throws SocketException { return delegate.getTrafficClass(); }
        @Override public boolean isBound() { return delegate.isBound(); }
        @Override public boolean isClosed() { return delegate.isClosed(); }
        @Override public boolean isConnected() { return delegate.isConnected(); }
        @Override public boolean isInputShutdown() { return delegate.isInputShutdown(); }
        @Override public boolean isOutputShutdown() { return delegate.isOutputShutdown(); }
        @Override public void sendUrgentData(int data) throws IOException { delegate.sendUrgentData(data); }
        @Override public void setKeepAlive(boolean on) throws SocketException { delegate.setKeepAlive(on); }
        @Override public void setOOBInline(boolean on) throws SocketException { delegate.setOOBInline(on); }
        @Override public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) { delegate.setPerformancePreferences(connectionTime, latency, bandwidth); }
        @Override public void setReceiveBufferSize(int size) throws SocketException { delegate.setReceiveBufferSize(size); }
        @Override public void setReuseAddress(boolean on) throws SocketException { delegate.setReuseAddress(on); }
        @Override public void setSendBufferSize(int size) throws SocketException { delegate.setSendBufferSize(size); }
        @Override public void setSoLinger(boolean on, int linger) throws SocketException { delegate.setSoLinger(on, linger); }
        @Override public void setSoTimeout(int timeout) throws SocketException { delegate.setSoTimeout(timeout); }
        @Override public void setTcpNoDelay(boolean on) throws SocketException { delegate.setTcpNoDelay(on); }
        @Override public void setTrafficClass(int tc) throws SocketException { delegate.setTrafficClass(tc); }
        @Override public void shutdownInput() throws IOException { delegate.shutdownInput(); }
        @Override public void shutdownOutput() throws IOException { delegate.shutdownOutput(); }
        @Override public String toString() { return delegate.toString(); }
    }

    /**
     * An {@link javax.net.ssl.SSLSocket} that delegates all calls.
     */
    private static abstract class DelegatingSSLSocket extends SSLSocket {
        protected final SSLSocket delegate;

        public DelegatingSSLSocket(SSLSocket delegate) {
            this.delegate = delegate;
        }

        @Override public void shutdownInput() throws IOException {
            delegate.shutdownInput();
        }

        @Override public void shutdownOutput() throws IOException {
            delegate.shutdownOutput();
        }

        @Override public String[] getSupportedCipherSuites() {
            return delegate.getSupportedCipherSuites();
        }

        @Override public String[] getEnabledCipherSuites() {
            return delegate.getEnabledCipherSuites();
        }

        @Override public void setEnabledCipherSuites(String[] suites) {
            delegate.setEnabledCipherSuites(suites);
        }

        @Override public String[] getSupportedProtocols() {
            return delegate.getSupportedProtocols();
        }

        @Override public String[] getEnabledProtocols() {
            return delegate.getEnabledProtocols();
        }

        @Override public void setEnabledProtocols(String[] protocols) {
            delegate.setEnabledProtocols(protocols);
        }

        @Override public SSLSession getSession() {
            return delegate.getSession();
        }

        @Override public void addHandshakeCompletedListener(HandshakeCompletedListener listener) {
            delegate.addHandshakeCompletedListener(listener);
        }

        @Override public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) {
            delegate.removeHandshakeCompletedListener(listener);
        }

        @Override public void startHandshake() throws IOException {
            delegate.startHandshake();
        }

        @Override public void setUseClientMode(boolean mode) {
            delegate.setUseClientMode(mode);
        }

        @Override public boolean getUseClientMode() {
            return delegate.getUseClientMode();
        }

        @Override public void setNeedClientAuth(boolean need) {
            delegate.setNeedClientAuth(need);
        }

        @Override public void setWantClientAuth(boolean want) {
            delegate.setWantClientAuth(want);
        }

        @Override public boolean getNeedClientAuth() {
            return delegate.getNeedClientAuth();
        }

        @Override public boolean getWantClientAuth() {
            return delegate.getWantClientAuth();
        }

        @Override public void setEnableSessionCreation(boolean flag) {
            delegate.setEnableSessionCreation(flag);
        }

        @Override public boolean getEnableSessionCreation() {
            return delegate.getEnableSessionCreation();
        }

        @Override public SSLParameters getSSLParameters() {
            return delegate.getSSLParameters();
        }

        @Override public void setSSLParameters(SSLParameters p) {
            delegate.setSSLParameters(p);
        }

        @Override public void close() throws IOException {
            delegate.close();
        }

        @Override public InetAddress getInetAddress() {
            return delegate.getInetAddress();
        }

        @Override public InputStream getInputStream() throws IOException {
            return delegate.getInputStream();
        }

        @Override public boolean getKeepAlive() throws SocketException {
            return delegate.getKeepAlive();
        }

        @Override public InetAddress getLocalAddress() {
            return delegate.getLocalAddress();
        }

        @Override public int getLocalPort() {
            return delegate.getLocalPort();
        }

        @Override public OutputStream getOutputStream() throws IOException {
            return delegate.getOutputStream();
        }

        @Override public int getPort() {
            return delegate.getPort();
        }

        @Override public int getSoLinger() throws SocketException {
            return delegate.getSoLinger();
        }

        @Override public int getReceiveBufferSize() throws SocketException {
            return delegate.getReceiveBufferSize();
        }

        @Override public int getSendBufferSize() throws SocketException {
            return delegate.getSendBufferSize();
        }

        @Override public int getSoTimeout() throws SocketException {
            return delegate.getSoTimeout();
        }

        @Override public boolean getTcpNoDelay() throws SocketException {
            return delegate.getTcpNoDelay();
        }

        @Override public void setKeepAlive(boolean keepAlive) throws SocketException {
            delegate.setKeepAlive(keepAlive);
        }

        @Override public void setSendBufferSize(int size) throws SocketException {
            delegate.setSendBufferSize(size);
        }

        @Override public void setReceiveBufferSize(int size) throws SocketException {
            delegate.setReceiveBufferSize(size);
        }

        @Override public void setSoLinger(boolean on, int timeout) throws SocketException {
            delegate.setSoLinger(on, timeout);
        }

        @Override public void setSoTimeout(int timeout) throws SocketException {
            delegate.setSoTimeout(timeout);
        }

        @Override public void setTcpNoDelay(boolean on) throws SocketException {
            delegate.setTcpNoDelay(on);
        }

        @Override public String toString() {
            return delegate.toString();
        }

        @Override public SocketAddress getLocalSocketAddress() {
            return delegate.getLocalSocketAddress();
        }

        @Override public SocketAddress getRemoteSocketAddress() {
            return delegate.getRemoteSocketAddress();
        }

        @Override public boolean isBound() {
            return delegate.isBound();
        }

        @Override public boolean isConnected() {
            return delegate.isConnected();
        }

        @Override public boolean isClosed() {
            return delegate.isClosed();
        }

        @Override public void bind(SocketAddress localAddr) throws IOException {
            delegate.bind(localAddr);
        }

        @Override public void connect(SocketAddress remoteAddr) throws IOException {
            delegate.connect(remoteAddr);
        }

        @Override public void connect(SocketAddress remoteAddr, int timeout) throws IOException {
            delegate.connect(remoteAddr, timeout);
        }

        @Override public boolean isInputShutdown() {
            return delegate.isInputShutdown();
        }

        @Override public boolean isOutputShutdown() {
            return delegate.isOutputShutdown();
        }

        @Override public void setReuseAddress(boolean reuse) throws SocketException {
            delegate.setReuseAddress(reuse);
        }

        @Override public boolean getReuseAddress() throws SocketException {
            return delegate.getReuseAddress();
        }

        @Override public void setOOBInline(boolean oobinline) throws SocketException {
            delegate.setOOBInline(oobinline);
        }

        @Override public boolean getOOBInline() throws SocketException {
            return delegate.getOOBInline();
        }

        @Override public void setTrafficClass(int value) throws SocketException {
            delegate.setTrafficClass(value);
        }

        @Override public int getTrafficClass() throws SocketException {
            return delegate.getTrafficClass();
        }

        @Override public void sendUrgentData(int value) throws IOException {
            delegate.sendUrgentData(value);
        }

        @Override public SocketChannel getChannel() {
            return delegate.getChannel();
        }

        @Override public void setPerformancePreferences(int connectionTime, int latency,
                int bandwidth) {
            delegate.setPerformancePreferences(connectionTime, latency, bandwidth);
        }
    }

    /**
     * An SSLSocketFactory that delegates calls. It keeps a record of any sockets created.
     * If {@link #disableTlsFallbackScsv} is set to {@code true} then sockets created by the
     * delegate are wrapped with ones that will not accept the {@link #TLS_FALLBACK_SCSV} cipher,
     * thus bypassing server-side fallback checks on platforms that support it. Unfortunately this
     * wrapping will disable any reflection-based calls to SSLSocket from Platform.
     */
    private static class FallbackTestClientSocketFactory extends DelegatingSSLSocketFactory {
        /**
         * The cipher suite used during TLS connection fallback to indicate a fallback.
         * See https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
         */
        public static final String TLS_FALLBACK_SCSV = "TLS_FALLBACK_SCSV";

        private final boolean disableTlsFallbackScsv;
        private final List<SSLSocket> createdSockets = new ArrayList<SSLSocket>();

        public FallbackTestClientSocketFactory(SSLSocketFactory delegate,
                boolean disableTlsFallbackScsv) {
            super(delegate);
            this.disableTlsFallbackScsv = disableTlsFallbackScsv;
        }

        @Override public SSLSocket createSocket(Socket s, String host, int port, boolean autoClose)
                throws IOException {
            SSLSocket socket = super.createSocket(s, host, port, autoClose);
            if (disableTlsFallbackScsv) {
                socket = new TlsFallbackDisabledScsvSSLSocket(socket);
            }
            createdSockets.add(socket);
            return socket;
        }

        @Override public SSLSocket createSocket() throws IOException {
            SSLSocket socket = super.createSocket();
            if (disableTlsFallbackScsv) {
                socket = new TlsFallbackDisabledScsvSSLSocket(socket);
            }
            createdSockets.add(socket);
            return socket;
        }

        @Override public SSLSocket createSocket(String host,int port) throws IOException {
            SSLSocket socket = super.createSocket(host, port);
            if (disableTlsFallbackScsv) {
                socket = new TlsFallbackDisabledScsvSSLSocket(socket);
            }
            createdSockets.add(socket);
            return socket;
        }

        @Override public SSLSocket createSocket(String host,int port, InetAddress localHost,
                int localPort) throws IOException {
            SSLSocket socket = super.createSocket(host, port, localHost, localPort);
            if (disableTlsFallbackScsv) {
                socket = new TlsFallbackDisabledScsvSSLSocket(socket);
            }
            createdSockets.add(socket);
            return socket;
        }

        @Override public SSLSocket createSocket(InetAddress host,int port) throws IOException {
            SSLSocket socket = super.createSocket(host, port);
            if (disableTlsFallbackScsv) {
                socket = new TlsFallbackDisabledScsvSSLSocket(socket);
            }
            createdSockets.add(socket);
            return socket;
        }

        @Override public SSLSocket createSocket(InetAddress address,int port,
                InetAddress localAddress, int localPort) throws IOException {
            SSLSocket socket = super.createSocket(address, port, localAddress, localPort);
            if (disableTlsFallbackScsv) {
                socket = new TlsFallbackDisabledScsvSSLSocket(socket);
            }
            createdSockets.add(socket);
            return socket;
        }

        public List<SSLSocket> getCreatedSockets() {
            return createdSockets;
        }
    }

    private static class TlsFallbackDisabledScsvSSLSocket extends DelegatingSSLSocket {

        private boolean tlsFallbackScsvSet;

        public TlsFallbackDisabledScsvSSLSocket(SSLSocket socket) {
            super(socket);
        }

        @Override public void setEnabledCipherSuites(String[] suites) {
            List<String> enabledCipherSuites = new ArrayList<String>(suites.length);
            for (String suite : suites) {
                if (suite.equals(FallbackTestClientSocketFactory.TLS_FALLBACK_SCSV)) {
                    // Record that an attempt was made to set TLS_FALLBACK_SCSV, but don't actually
                    // set it.
                    tlsFallbackScsvSet = true;
                } else {
                    enabledCipherSuites.add(suite);
                }
            }
            delegate.setEnabledCipherSuites(
                    enabledCipherSuites.toArray(new String[enabledCipherSuites.size()]));
        }

        public boolean wasTlsFallbackScsvSet() {
            return tlsFallbackScsvSet;
        }
    }
}
