ObsoleteUrlFactory.java

package org.kohsuke.github.extras.okhttp3;

/*
 * Copyright (C) 2014 Square, Inc.
 *
 * 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.
 */

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Dispatcher;
import okhttp3.Handshake;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSink;
import okio.Okio;
import okio.Pipe;
import okio.Timeout;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.SocketPermission;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.security.AccessControlException;
import java.security.Permission;
import java.security.Principal;
import java.security.cert.Certificate;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;

import javax.annotation.Nullable;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;

import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;

/**
 * OkHttp 3.14 dropped support for the long-deprecated OkUrlFactory class, which allows you to use the HttpURLConnection
 * API with OkHttp's implementation. This class does the same thing using only public APIs in OkHttp. It requires OkHttp
 * 3.14 or newer.
 *
 * <p>
 * Rather than pasting this 1100 line gist into your source code, please upgrade to OkHttp's request/response API. Your
 * code will be shorter, easier to read, and you'll be able to use interceptors.
 */
public final class ObsoleteUrlFactory implements URLStreamHandlerFactory, Cloneable {
    static final String SELECTED_PROTOCOL = "ObsoleteUrlFactory-Selected-Protocol";

    static final String RESPONSE_SOURCE = "ObsoleteUrlFactory-Response-Source";

    static final Set<String> METHODS = new LinkedHashSet<>(
            Arrays.asList("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "PATCH"));

    static final TimeZone UTC = TimeZone.getTimeZone("GMT");

    static final int HTTP_CONTINUE = 100;

    static final ThreadLocal<DateFormat> STANDARD_DATE_FORMAT = ThreadLocal.withInitial(() -> {
        // Date format specified by RFC 7231 section 7.1.1.1.
        DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
        rfc1123.setLenient(false);
        rfc1123.setTimeZone(UTC);
        return rfc1123;
    });

    static final Comparator<String> FIELD_NAME_COMPARATOR = (a, b) -> {
        if (Objects.equals(a, b)) {
            return 0;
        } else if (Objects.isNull(a)) {
            return -1;
        } else if (Objects.isNull(b)) {
            return 1;
        } else {
            return String.CASE_INSENSITIVE_ORDER.compare(a, b);
        }
    };

    private OkHttpClient client;

    /**
     * Instantiates a new Obsolete url factory.
     *
     * @param client
     *            the client
     */
    public ObsoleteUrlFactory(OkHttpClient client) {
        this.client = client;
    }

    /**
     * Client ok http client.
     *
     * @return the ok http client
     */
    public OkHttpClient client() {
        return client;
    }

    /**
     * Sets client.
     *
     * @param client
     *            the client
     * @return the client
     */
    public ObsoleteUrlFactory setClient(OkHttpClient client) {
        this.client = client;
        return this;
    }

    /**
     * Returns a copy of this stream handler factory that includes a shallow copy of the internal
     * {@linkplain OkHttpClient HTTP client}.
     */
    @Override
    public ObsoleteUrlFactory clone() {
        return new ObsoleteUrlFactory(client);
    }

    /**
     * Open http url connection.
     *
     * @param url
     *            the url
     * @return the http url connection
     */
    public HttpURLConnection open(URL url) {
        return open(url, client.proxy());
    }

    HttpURLConnection open(URL url, @Nullable Proxy proxy) {
        String protocol = url.getProtocol();
        OkHttpClient copy = client.newBuilder().proxy(proxy).build();

        if (protocol.equals("http"))
            return new OkHttpURLConnection(url, copy);
        if (protocol.equals("https"))
            return new OkHttpsURLConnection(url, copy);
        throw new IllegalArgumentException("Unexpected protocol: " + protocol);
    }

    /**
     * Creates a URLStreamHandler as a {@link java.net.URL#setURLStreamHandlerFactory}.
     *
     * <p>
     * This code configures OkHttp to handle all HTTP and HTTPS connections created with
     * {@link java.net.URL#openConnection()}:
     *
     * <pre>
     * {
     *     &#64;code
     *
     *     OkHttpClient okHttpClient = new OkHttpClient();
     *     URL.setURLStreamHandlerFactory(new ObsoleteUrlFactory(okHttpClient));
     * }
     * </pre>
     */
    @Override
    public URLStreamHandler createURLStreamHandler(final String protocol) {
        if (!protocol.equals("http") && !protocol.equals("https"))
            return null;

        return new URLStreamHandler() {
            @Override
            protected URLConnection openConnection(URL url) {
                return open(url);
            }

            @Override
            protected URLConnection openConnection(URL url, Proxy proxy) {
                return open(url, proxy);
            }

            @Override
            protected int getDefaultPort() {
                if (protocol.equals("http"))
                    return 80;
                if (protocol.equals("https"))
                    return 443;
                throw new AssertionError();
            }
        };
    }

    static String format(Date value) {
        return STANDARD_DATE_FORMAT.get().format(value);
    }

    static boolean permitsRequestBody(String method) {
        return !(method.equals("GET") || method.equals("HEAD"));
    }

    /** Returns true if the response must have a (possibly 0-length) body. See RFC 7231. */
    static boolean hasBody(Response response) {
        // HEAD requests never yield a body regardless of the response headers.
        if (response.request().method().equals("HEAD")) {
            return false;
        }

        int responseCode = response.code();
        if ((responseCode < HTTP_CONTINUE || responseCode >= 200) && responseCode != HTTP_NO_CONTENT
                && responseCode != HTTP_NOT_MODIFIED) {
            return true;
        }

        // If the Content-Length or Transfer-Encoding headers disagree with the response code, the
        // response is malformed. For best compatibility, we honor the headers.
        if (contentLength(response.headers()) != -1
                || "chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
            return true;
        }

        return false;
    }

    static long contentLength(Headers headers) {
        String s = headers.get("Content-Length");
        if (s == null)
            return -1;
        try {
            return Long.parseLong(s);
        } catch (NumberFormatException e) {
            return -1;
        }
    }

    static String responseSourceHeader(Response response) {
        Response networkResponse = response.networkResponse();
        if (networkResponse == null) {
            return response.cacheResponse() == null ? "NONE" : "CACHE " + response.code();
        } else {
            return response.cacheResponse() == null
                    ? "NETWORK " + response.code()
                    : "CONDITIONAL_CACHE " + networkResponse.code();
        }
    }

    static String statusLineToString(Response response) {
        return (response.protocol() == Protocol.HTTP_1_0 ? "HTTP/1.0" : "HTTP/1.1") + ' ' + response.code() + ' '
                + response.message();
    }

    static String toHumanReadableAscii(String s) {
        for (int i = 0, length = s.length(), c; i < length; i += Character.charCount(c)) {
            c = s.codePointAt(i);
            if (c > '\u001f' && c < '\u007f')
                continue;

            try (Buffer buffer = new Buffer()) {
                buffer.writeUtf8(s, 0, i);
                buffer.writeUtf8CodePoint('?');
                for (int j = i + Character.charCount(c); j < length; j += Character.charCount(c)) {
                    c = s.codePointAt(j);
                    buffer.writeUtf8CodePoint(c > '\u001f' && c < '\u007f' ? c : '?');
                }
                return buffer.readUtf8();
            }
        }
        return s;
    }

    static Map<String, List<String>> toMultimap(Headers headers, @Nullable String valueForNullKey) {
        Map<String, List<String>> result = new TreeMap<>(FIELD_NAME_COMPARATOR);
        for (int i = 0, size = headers.size(); i < size; i++) {
            String fieldName = headers.name(i);
            String value = headers.value(i);

            List<String> allValues = new ArrayList<>();
            List<String> otherValues = result.get(fieldName);
            if (otherValues != null) {
                allValues.addAll(otherValues);
            }
            allValues.add(value);
            result.put(fieldName, Collections.unmodifiableList(allValues));
        }
        if (valueForNullKey != null) {
            result.put(null, Collections.unmodifiableList(Collections.singletonList(valueForNullKey)));
        }
        return Collections.unmodifiableMap(result);
    }

    static String getSystemProperty(String key, @Nullable String defaultValue) {
        String value;
        try {
            value = System.getProperty(key);
        } catch (AccessControlException ex) {
            return defaultValue;
        }
        return value != null ? value : defaultValue;
    }

    static String defaultUserAgent() {
        String agent = getSystemProperty("http.agent", null);
        return agent != null ? toHumanReadableAscii(agent) : "ObsoleteUrlFactory";
    }

    static IOException propagate(Throwable throwable) throws IOException {
        if (throwable instanceof IOException)
            throw (IOException) throwable;
        if (throwable instanceof Error)
            throw (Error) throwable;
        if (throwable instanceof RuntimeException)
            throw (RuntimeException) throwable;
        throw new AssertionError();
    }

    static final class OkHttpURLConnection extends HttpURLConnection implements Callback {
        // These fields are confined to the application thread that uses HttpURLConnection.
        OkHttpClient client;
        final NetworkInterceptor networkInterceptor = new NetworkInterceptor();
        Headers.Builder requestHeaders = new Headers.Builder();
        Headers responseHeaders;
        boolean executed;
        Call call;

        /** Like the superclass field of the same name, but a long and available on all platforms. */
        long fixedContentLength = -1L;

        // These fields are guarded by lock.
        private final Object lock = new Object();
        private Response response;
        private Throwable callFailure;
        Response networkResponse;
        boolean connectPending = true;
        Proxy proxy;
        Handshake handshake;

        OkHttpURLConnection(URL url, OkHttpClient client) {
            super(url);
            this.client = client;
        }

        @Override
        public void connect() throws IOException {
            if (executed)
                return;

            Call call = buildCall();
            executed = true;
            call.enqueue(this);

            synchronized (lock) {
                try {
                    while (connectPending && response == null && callFailure == null) {
                        lock.wait(); // Wait 'til the network interceptor is reached or the call fails.
                    }
                    if (callFailure != null) {
                        throw propagate(callFailure);
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // Retain interrupted status.
                    throw new InterruptedIOException();
                }
            }
        }

        @Override
        public void disconnect() {
            // Calling disconnect() before a connection exists should have no effect.
            if (call == null)
                return;

            networkInterceptor.proceed(); // Unblock any waiting async thread.
            call.cancel();
        }

        @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "hasBody checks for this")
        @Override
        public InputStream getErrorStream() {
            try {
                Response response = getResponse(true);
                if (hasBody(response) && response.code() >= HTTP_BAD_REQUEST) {
                    return new ResponseBodyInputStream(response.body());
                }
                return null;
            } catch (IOException e) {
                return null;
            }
        }

        Headers getHeaders() throws IOException {
            if (responseHeaders == null) {
                Response response = getResponse(true);
                Headers headers = response.headers();
                responseHeaders = headers.newBuilder()
                        .add(SELECTED_PROTOCOL, response.protocol().toString())
                        .add(RESPONSE_SOURCE, responseSourceHeader(response))
                        .build();
            }
            return responseHeaders;
        }

        @Override
        public String getHeaderField(int position) {
            try {
                Headers headers = getHeaders();
                if (position < 0 || position >= headers.size())
                    return null;
                return headers.value(position);
            } catch (IOException e) {
                return null;
            }
        }

        @Override
        public String getHeaderField(String fieldName) {
            try {
                return fieldName == null ? statusLineToString(getResponse(true)) : getHeaders().get(fieldName);
            } catch (IOException e) {
                return null;
            }
        }

        @Override
        public String getHeaderFieldKey(int position) {
            try {
                Headers headers = getHeaders();
                if (position < 0 || position >= headers.size())
                    return null;
                return headers.name(position);
            } catch (IOException e) {
                return null;
            }
        }

        @Override
        public Map<String, List<String>> getHeaderFields() {
            try {
                return toMultimap(getHeaders(), statusLineToString(getResponse(true)));
            } catch (IOException e) {
                return Collections.emptyMap();
            }
        }

        @Override
        public Map<String, List<String>> getRequestProperties() {
            if (connected) {
                throw new IllegalStateException("Cannot access request header fields after connection is set");
            }

            return toMultimap(requestHeaders.build(), null);
        }

        @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE",
                justification = "Good request will have body")
        @Override
        public InputStream getInputStream() throws IOException {
            if (!doInput) {
                throw new ProtocolException("This protocol does not support input");
            }

            Response response = getResponse(false);
            if (response.code() >= HTTP_BAD_REQUEST)
                throw new FileNotFoundException(url.toString());
            return new ResponseBodyInputStream(response.body());
        }

        @Override
        public OutputStream getOutputStream() throws IOException {
            OutputStreamRequestBody requestBody = (OutputStreamRequestBody) buildCall().request().body();
            if (requestBody == null) {
                throw new ProtocolException("method does not support a request body: " + method);
            }

            if (requestBody instanceof StreamedRequestBody) {
                connect();
                networkInterceptor.proceed();
            }

            if (requestBody.closed) {
                throw new ProtocolException("cannot write request body after response has been read");
            }

            return requestBody.outputStream;
        }

        @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE",
                justification = "usingProxy() handles this")
        @Override
        public Permission getPermission() {
            URL url = getURL();
            String hostname = url.getHost();
            int hostPort = url.getPort() != -1 ? url.getPort() : HttpUrl.defaultPort(url.getProtocol());
            if (usingProxy()) {
                InetSocketAddress proxyAddress = (InetSocketAddress) client.proxy().address();
                hostname = proxyAddress.getHostName();
                hostPort = proxyAddress.getPort();
            }
            return new SocketPermission(hostname + ":" + hostPort, "connect, resolve");
        }

        @Override
        public String getRequestProperty(String field) {
            if (field == null)
                return null;
            return requestHeaders.get(field);
        }

        @Override
        public void setConnectTimeout(int timeoutMillis) {
            client = client.newBuilder().connectTimeout(timeoutMillis, TimeUnit.MILLISECONDS).build();
        }

        @Override
        public void setInstanceFollowRedirects(boolean followRedirects) {
            client = client.newBuilder().followRedirects(followRedirects).build();
        }

        @Override
        public boolean getInstanceFollowRedirects() {
            return client.followRedirects();
        }

        @Override
        public int getConnectTimeout() {
            return client.connectTimeoutMillis();
        }

        @Override
        public void setReadTimeout(int timeoutMillis) {
            client = client.newBuilder().readTimeout(timeoutMillis, TimeUnit.MILLISECONDS).build();
        }

        @Override
        public int getReadTimeout() {
            return client.readTimeoutMillis();
        }

        @SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE")
        private Call buildCall() throws IOException {
            if (call != null) {
                return call;
            }

            connected = true;
            if (doOutput) {
                if (method.equals("GET")) {
                    method = "POST";
                } else if (!permitsRequestBody(method)) {
                    throw new ProtocolException(method + " does not support writing");
                }
            }

            if (requestHeaders.get("User-Agent") == null) {
                requestHeaders.add("User-Agent", defaultUserAgent());
            }

            OutputStreamRequestBody requestBody = null;
            if (permitsRequestBody(method)) {
                String contentType = requestHeaders.get("Content-Type");
                if (contentType == null) {
                    contentType = "application/x-www-form-urlencoded";
                    requestHeaders.add("Content-Type", contentType);
                }

                boolean stream = fixedContentLength != -1L || chunkLength > 0;

                long contentLength = -1L;
                String contentLengthString = requestHeaders.get("Content-Length");
                if (fixedContentLength != -1L) {
                    contentLength = fixedContentLength;
                } else if (contentLengthString != null) {
                    contentLength = Long.parseLong(contentLengthString);
                }

                requestBody = stream ? new StreamedRequestBody(contentLength) : new BufferedRequestBody(contentLength);
                requestBody.timeout.timeout(client.writeTimeoutMillis(), TimeUnit.MILLISECONDS);
            }

            HttpUrl url;
            try {
                url = HttpUrl.get(getURL().toString());
            } catch (IllegalArgumentException e) {
                MalformedURLException malformedUrl = new MalformedURLException();
                malformedUrl.initCause(e);
                throw malformedUrl;
            }

            Request request = new Request.Builder().url(url)
                    .headers(requestHeaders.build())
                    .method(method, requestBody)
                    .build();

            OkHttpClient.Builder clientBuilder = client.newBuilder();
            clientBuilder.interceptors().clear();
            clientBuilder.interceptors().add(UnexpectedException.INTERCEPTOR);
            clientBuilder.networkInterceptors().clear();
            clientBuilder.networkInterceptors().add(networkInterceptor);

            // Use a separate dispatcher so that limits aren't impacted. But use the same executor service!
            clientBuilder.dispatcher(new Dispatcher(client.dispatcher().executorService()));

            // If we're currently not using caches, make sure the engine's client doesn't have one.
            if (!getUseCaches()) {
                clientBuilder.cache(null);
            }

            return call = clientBuilder.build().newCall(request);
        }

        private Response getResponse(boolean networkResponseOnError) throws IOException {
            synchronized (lock) {
                if (response != null)
                    return response;
                if (callFailure != null) {
                    if (networkResponseOnError && networkResponse != null)
                        return networkResponse;
                    throw propagate(callFailure);
                }
            }

            Call call = buildCall();
            networkInterceptor.proceed();

            OutputStreamRequestBody requestBody = (OutputStreamRequestBody) call.request().body();
            if (requestBody != null)
                requestBody.outputStream.close();

            if (executed) {
                synchronized (lock) {
                    try {
                        while (response == null && callFailure == null) {
                            lock.wait(); // Wait until the response is returned or the call fails.
                        }
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt(); // Retain interrupted status.
                        throw new InterruptedIOException();
                    }
                }
            } else {
                executed = true;
                try {
                    onResponse(call, call.execute());
                } catch (IOException e) {
                    onFailure(call, e);
                }
            }

            synchronized (lock) {
                if (callFailure != null)
                    throw propagate(callFailure);
                if (response != null)
                    return response;
            }

            throw new AssertionError();
        }

        @Override
        public boolean usingProxy() {
            if (proxy != null)
                return true;
            Proxy clientProxy = client.proxy();
            return clientProxy != null && clientProxy.type() != Proxy.Type.DIRECT;
        }

        @Override
        public String getResponseMessage() throws IOException {
            return getResponse(true).message();
        }

        @Override
        public int getResponseCode() throws IOException {
            return getResponse(true).code();
        }

        @Override
        public void setRequestProperty(String field, String newValue) {
            if (connected) {
                throw new IllegalStateException("Cannot set request property after connection is made");
            }
            if (field == null) {
                throw new NullPointerException("field == null");
            }
            if (newValue == null) {
                return;
            }

            requestHeaders.set(field, newValue);
        }

        @Override
        public void setIfModifiedSince(long newValue) {
            super.setIfModifiedSince(newValue);
            if (ifModifiedSince != 0) {
                requestHeaders.set("If-Modified-Since", format(new Date(ifModifiedSince)));
            } else {
                requestHeaders.removeAll("If-Modified-Since");
            }
        }

        @Override
        public void addRequestProperty(String field, String value) {
            if (connected) {
                throw new IllegalStateException("Cannot add request property after connection is made");
            }
            if (field == null) {
                throw new NullPointerException("field == null");
            }
            if (value == null) {
                return;
            }

            requestHeaders.add(field, value);
        }

        @Override
        public void setRequestMethod(String method) throws ProtocolException {
            if (!METHODS.contains(method)) {
                throw new ProtocolException("Expected one of " + METHODS + " but was " + method);
            }
            this.method = method;
        }

        @Override
        public void setFixedLengthStreamingMode(int contentLength) {
            setFixedLengthStreamingMode((long) contentLength);
        }

        @Override
        public void setFixedLengthStreamingMode(long contentLength) {
            if (super.connected)
                throw new IllegalStateException("Already connected");
            if (chunkLength > 0)
                throw new IllegalStateException("Already in chunked mode");
            if (contentLength < 0)
                throw new IllegalArgumentException("contentLength < 0");
            this.fixedContentLength = contentLength;
            super.fixedContentLength = (int) Math.min(contentLength, Integer.MAX_VALUE);
        }

        @Override
        public void onFailure(Call call, IOException e) {
            synchronized (lock) {
                this.callFailure = (e instanceof UnexpectedException) ? e.getCause() : e;
                lock.notifyAll();
            }
        }

        @Override
        public void onResponse(Call call, Response response) {
            synchronized (lock) {
                this.response = response;
                this.handshake = response.handshake();
                this.url = response.request().url().url();
                lock.notifyAll();
            }
        }

        final class NetworkInterceptor implements Interceptor {
            // Guarded by HttpUrlConnection.this.
            private boolean proceed;

            /**
             * Proceed.
             */
            public void proceed() {
                synchronized (lock) {
                    this.proceed = true;
                    lock.notifyAll();
                }
            }

            @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE",
                    justification = "If we get here there is a connection and request.body() is checked")
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();

                synchronized (lock) {
                    connectPending = false;
                    proxy = chain.connection().route().proxy();
                    handshake = chain.connection().handshake();
                    lock.notifyAll();

                    try {
                        while (!proceed) {
                            lock.wait(); // Wait until proceed() is called.
                        }
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt(); // Retain interrupted status.
                        throw new InterruptedIOException();
                    }
                }

                // Try to lock in the Content-Length before transmitting the request body.
                if (request.body() instanceof OutputStreamRequestBody) {
                    OutputStreamRequestBody requestBody = (OutputStreamRequestBody) request.body();
                    request = requestBody.prepareToSendRequest(request);
                }

                Response response = chain.proceed(request);

                synchronized (lock) {
                    networkResponse = response;
                    url = response.request().url().url();
                }

                return response;
            }
        }
    }

    abstract static class OutputStreamRequestBody extends RequestBody {
        Timeout timeout;
        long expectedContentLength;
        OutputStream outputStream;
        boolean closed;

        void initOutputStream(BufferedSink sink, long expectedContentLength) {
            this.timeout = sink.timeout();
            this.expectedContentLength = expectedContentLength;

            // An output stream that writes to sink. If expectedContentLength is not -1, then this expects
            // exactly that many bytes to be written.
            this.outputStream = new OutputStream() {
                private long bytesReceived;

                @Override
                public void write(int b) throws IOException {
                    write(new byte[]{ (byte) b }, 0, 1);
                }

                @Override
                public void write(byte[] source, int offset, int byteCount) throws IOException {
                    if (closed)
                        throw new IOException("closed"); // Not IllegalStateException!

                    if (expectedContentLength != -1L && bytesReceived + byteCount > expectedContentLength) {
                        throw new ProtocolException("expected " + expectedContentLength + " bytes but received "
                                + bytesReceived + byteCount);
                    }

                    bytesReceived += byteCount;
                    try {
                        sink.write(source, offset, byteCount);
                    } catch (InterruptedIOException e) {
                        throw new SocketTimeoutException(e.getMessage());
                    }
                }

                @Override
                public void flush() throws IOException {
                    if (closed)
                        return; // Weird, but consistent with historical behavior.
                    sink.flush();
                }

                @Override
                public void close() throws IOException {
                    closed = true;

                    if (expectedContentLength != -1L && bytesReceived < expectedContentLength) {
                        throw new ProtocolException(
                                "expected " + expectedContentLength + " bytes but received " + bytesReceived);
                    }

                    sink.close();
                }
            };
        }

        @Override
        public long contentLength() {
            return expectedContentLength;
        }

        @Override
        public final @Nullable MediaType contentType() {
            return null; // Let the caller provide this in a regular header.
        }

        /**
         * Prepare to send request request.
         *
         * @param request
         *            the request
         * @return the request
         * @throws IOException
         *             the io exception
         */
        public Request prepareToSendRequest(Request request) throws IOException {
            return request;
        }
    }

    static final class BufferedRequestBody extends OutputStreamRequestBody {
        final Buffer buffer = new Buffer();
        long contentLength = -1L;

        BufferedRequestBody(long expectedContentLength) {
            initOutputStream(buffer, expectedContentLength);
        }

        @Override
        public long contentLength() {
            return contentLength;
        }

        @Override
        public Request prepareToSendRequest(Request request) throws IOException {
            if (request.header("Content-Length") != null)
                return request;

            outputStream.close();
            contentLength = buffer.size();
            return request.newBuilder()
                    .removeHeader("Transfer-Encoding")
                    .header("Content-Length", Long.toString(buffer.size()))
                    .build();
        }

        @Override
        public void writeTo(BufferedSink sink) {
            buffer.copyTo(sink.buffer(), 0, buffer.size());
        }
    }

    static final class StreamedRequestBody extends OutputStreamRequestBody {
        private final Pipe pipe = new Pipe(8192);

        StreamedRequestBody(long expectedContentLength) {
            initOutputStream(Okio.buffer(pipe.sink()), expectedContentLength);
        }

        @Override
        public boolean isOneShot() {
            return true;
        }

        @Override
        public void writeTo(BufferedSink sink) throws IOException {
            Buffer buffer = new Buffer();
            while (pipe.source().read(buffer, 8192) != -1L) {
                sink.write(buffer, buffer.size());
            }
        }
    }

    abstract static class DelegatingHttpsURLConnection extends HttpsURLConnection {
        private final HttpURLConnection delegate;

        DelegatingHttpsURLConnection(HttpURLConnection delegate) {
            super(delegate.getURL());
            this.delegate = delegate;
        }

        /**
         * Handshake handshake.
         *
         * @return the handshake
         */
        protected abstract Handshake handshake();

        @Override
        public abstract void setHostnameVerifier(HostnameVerifier hostnameVerifier);

        @Override
        public abstract HostnameVerifier getHostnameVerifier();

        @Override
        public abstract void setSSLSocketFactory(SSLSocketFactory sslSocketFactory);

        @Override
        public abstract SSLSocketFactory getSSLSocketFactory();

        @Override
        public String getCipherSuite() {
            Handshake handshake = handshake();
            return handshake != null ? handshake.cipherSuite().javaName() : null;
        }

        @Override
        public Certificate[] getLocalCertificates() {
            Handshake handshake = handshake();
            if (handshake == null)
                return null;
            List<Certificate> result = handshake.localCertificates();
            return !result.isEmpty() ? result.toArray(new Certificate[result.size()]) : null;
        }

        @Override
        public Certificate[] getServerCertificates() {
            Handshake handshake = handshake();
            if (handshake == null)
                return null;
            List<Certificate> result = handshake.peerCertificates();
            return !result.isEmpty() ? result.toArray(new Certificate[result.size()]) : null;
        }

        @Override
        public Principal getPeerPrincipal() {
            Handshake handshake = handshake();
            return handshake != null ? handshake.peerPrincipal() : null;
        }

        @Override
        public Principal getLocalPrincipal() {
            Handshake handshake = handshake();
            return handshake != null ? handshake.localPrincipal() : null;
        }

        @Override
        public void connect() throws IOException {
            connected = true;
            delegate.connect();
        }

        @Override
        public void disconnect() {
            delegate.disconnect();
        }

        @Override
        public InputStream getErrorStream() {
            return delegate.getErrorStream();
        }

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

        @Override
        public int getResponseCode() throws IOException {
            return delegate.getResponseCode();
        }

        @Override
        public String getResponseMessage() throws IOException {
            return delegate.getResponseMessage();
        }

        @Override
        public void setRequestMethod(String method) throws ProtocolException {
            delegate.setRequestMethod(method);
        }

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

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

        @Override
        public void setInstanceFollowRedirects(boolean followRedirects) {
            delegate.setInstanceFollowRedirects(followRedirects);
        }

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

        @Override
        public Object getContent() throws IOException {
            return delegate.getContent();
        }

        @Override
        public Object getContent(Class[] types) throws IOException {
            return delegate.getContent(types);
        }

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

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

        // Should only be invoked on Java 8+ or Android API 24+.
        @Override
        public long getContentLengthLong() {
            return delegate.getContentLengthLong();
        }

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

        @Override
        public long getDate() {
            return delegate.getDate();
        }

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

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

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

        @Override
        public long getExpiration() {
            return delegate.getExpiration();
        }

        @Override
        public String getHeaderField(int pos) {
            return delegate.getHeaderField(pos);
        }

        @Override
        public Map<String, List<String>> getHeaderFields() {
            return delegate.getHeaderFields();
        }

        @Override
        public Map<String, List<String>> getRequestProperties() {
            return delegate.getRequestProperties();
        }

        @Override
        public void addRequestProperty(String field, String newValue) {
            delegate.addRequestProperty(field, newValue);
        }

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

        // Should only be invoked on Java 8+ or Android API 24+.
        @Override
        public long getHeaderFieldLong(String field, long defaultValue) {
            return delegate.getHeaderFieldLong(field, defaultValue);
        }

        @Override
        public long getHeaderFieldDate(String field, long defaultValue) {
            return delegate.getHeaderFieldDate(field, defaultValue);
        }

        @Override
        public int getHeaderFieldInt(String field, int defaultValue) {
            return delegate.getHeaderFieldInt(field, defaultValue);
        }

        @Override
        public String getHeaderFieldKey(int position) {
            return delegate.getHeaderFieldKey(position);
        }

        @Override
        public long getIfModifiedSince() {
            return delegate.getIfModifiedSince();
        }

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

        @Override
        public long getLastModified() {
            return delegate.getLastModified();
        }

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

        @Override
        public Permission getPermission() throws IOException {
            return delegate.getPermission();
        }

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

        @Override
        public URL getURL() {
            return delegate.getURL();
        }

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

        @Override
        public void setAllowUserInteraction(boolean newValue) {
            delegate.setAllowUserInteraction(newValue);
        }

        @Override
        public void setDefaultUseCaches(boolean newValue) {
            delegate.setDefaultUseCaches(newValue);
        }

        @Override
        public void setDoInput(boolean newValue) {
            delegate.setDoInput(newValue);
        }

        @Override
        public void setDoOutput(boolean newValue) {
            delegate.setDoOutput(newValue);
        }

        // Should only be invoked on Java 8+ or Android API 24+.
        @Override
        public void setFixedLengthStreamingMode(long contentLength) {
            delegate.setFixedLengthStreamingMode(contentLength);
        }

        @Override
        public void setIfModifiedSince(long newValue) {
            delegate.setIfModifiedSince(newValue);
        }

        @Override
        public void setRequestProperty(String field, String newValue) {
            delegate.setRequestProperty(field, newValue);
        }

        @Override
        public void setUseCaches(boolean newValue) {
            delegate.setUseCaches(newValue);
        }

        @Override
        public void setConnectTimeout(int timeoutMillis) {
            delegate.setConnectTimeout(timeoutMillis);
        }

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

        @Override
        public void setReadTimeout(int timeoutMillis) {
            delegate.setReadTimeout(timeoutMillis);
        }

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

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

        @Override
        public void setFixedLengthStreamingMode(int contentLength) {
            delegate.setFixedLengthStreamingMode(contentLength);
        }

        @Override
        public void setChunkedStreamingMode(int chunkLength) {
            delegate.setChunkedStreamingMode(chunkLength);
        }
    }

    static final class OkHttpsURLConnection extends DelegatingHttpsURLConnection {
        private final OkHttpURLConnection delegate;

        OkHttpsURLConnection(URL url, OkHttpClient client) {
            this(new OkHttpURLConnection(url, client));
        }

        OkHttpsURLConnection(OkHttpURLConnection delegate) {
            super(delegate);
            this.delegate = delegate;
        }

        @Override
        protected Handshake handshake() {
            if (delegate.call == null) {
                throw new IllegalStateException("Connection has not yet been established");
            }

            return delegate.handshake;
        }

        @Override
        public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
            delegate.client = delegate.client.newBuilder().hostnameVerifier(hostnameVerifier).build();
        }

        @Override
        public HostnameVerifier getHostnameVerifier() {
            return delegate.client.hostnameVerifier();
        }

        @Override
        public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
            if (sslSocketFactory == null) {
                throw new IllegalArgumentException("sslSocketFactory == null");
            }
            // This fails in JDK 9 because OkHttp is unable to extract the trust manager.
            delegate.client = delegate.client.newBuilder().sslSocketFactory(sslSocketFactory).build();
        }

        @Override
        public SSLSocketFactory getSSLSocketFactory() {
            return delegate.client.sslSocketFactory();
        }
    }

    static final class UnexpectedException extends IOException {
        static final Interceptor INTERCEPTOR = chain -> {
            try {
                return chain.proceed(chain.request());
            } catch (Error | RuntimeException e) {
                throw new UnexpectedException(e);
            }
        };

        UnexpectedException(Throwable cause) {
            super(cause);
        }
    }

    /**
     * Make sure both the ResponseBody and the InputStream are closed when the InputStream coming from the ResponseBody
     * is closed.
     */
    private static final class ResponseBodyInputStream extends InputStream {

        private final ResponseBody responseBody;

        private final InputStream inputStream;

        private ResponseBodyInputStream(ResponseBody responseBody) {
            this.responseBody = responseBody;
            this.inputStream = responseBody.byteStream();
        }

        @Override
        public int read() throws IOException {
            return inputStream.read();
        }

        @Override
        public int read(byte b[]) throws IOException {
            return inputStream.read(b);
        }

        @Override
        public int read(byte b[], int off, int len) throws IOException {
            return inputStream.read(b, off, len);
        }

        @Override
        public long skip(long n) throws IOException {
            return inputStream.skip(n);
        }

        @Override
        public int available() throws IOException {
            return inputStream.available();
        }

        @Override
        public synchronized void mark(int readlimit) {
            inputStream.mark(readlimit);
        }

        @Override
        public synchronized void reset() throws IOException {
            inputStream.reset();
        }

        @Override
        public boolean markSupported() {
            return inputStream.markSupported();
        }

        @Override
        public void close() throws IOException {
            try {
                inputStream.close();
            } finally {
                responseBody.close();
            }
        }
    }
}