/* * 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. */ package okhttp3.internal.huc; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.CacheResponse; import java.net.HttpURLConnection; import java.net.ProtocolException; import java.net.SecureCacheResponse; import java.net.URI; import java.net.URLConnection; import java.security.Principal; import java.security.cert.Certificate; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSocketFactory; import okhttp3.CipherSuite; import okhttp3.Handshake; import okhttp3.Headers; import okhttp3.MediaType; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; import okhttp3.TlsVersion; import okhttp3.internal.Internal; import okhttp3.internal.JavaNetHeaders; import okhttp3.internal.Util; import okhttp3.internal.cache.CacheRequest; import okhttp3.internal.http.HttpHeaders; import okhttp3.internal.http.HttpMethod; import okhttp3.internal.http.StatusLine; import okhttp3.internal.platform.Platform; import okio.BufferedSource; import okio.Okio; import okio.Sink; /** * Helper methods that convert between Java and OkHttp representations. */ public final class JavaApiConverter { /** Synthetic response header: the local time when the request was sent. */ private static final String SENT_MILLIS = Platform.get().getPrefix() + "-Sent-Millis"; /** Synthetic response header: the local time when the response was received. */ private static final String RECEIVED_MILLIS = Platform.get().getPrefix() + "-Received-Millis"; private JavaApiConverter() { } /** * Creates an OkHttp {@link Response} using the supplied {@link URI} and {@link URLConnection} to * supply the data. The URLConnection is assumed to already be connected. If this method returns * {@code null} the response is uncacheable. */ public static Response createOkResponseForCachePut(URI uri, URLConnection urlConnection) throws IOException { HttpURLConnection httpUrlConnection = (HttpURLConnection) urlConnection; Response.Builder okResponseBuilder = new Response.Builder(); // Request: Create one from the URL connection. Headers responseHeaders = createHeaders(urlConnection.getHeaderFields()); // Some request headers are needed for Vary caching. Headers varyHeaders = varyHeaders(urlConnection, responseHeaders); if (varyHeaders == null) { return null; } // OkHttp's Call API requires a placeholder body; the real body will be streamed separately. String requestMethod = httpUrlConnection.getRequestMethod(); RequestBody placeholderBody = HttpMethod.requiresRequestBody(requestMethod) ? Util.EMPTY_REQUEST : null; Request okRequest = new Request.Builder() .url(uri.toString()) .method(requestMethod, placeholderBody) .headers(varyHeaders) .build(); okResponseBuilder.request(okRequest); // Status line StatusLine statusLine = StatusLine.parse(extractStatusLine(httpUrlConnection)); okResponseBuilder.protocol(statusLine.protocol); okResponseBuilder.code(statusLine.code); okResponseBuilder.message(statusLine.message); // A network response is required for the Cache to find any Vary headers it needs. Response networkResponse = okResponseBuilder.build(); okResponseBuilder.networkResponse(networkResponse); // Response headers Headers okHeaders = extractOkResponseHeaders(httpUrlConnection, okResponseBuilder); okResponseBuilder.headers(okHeaders); // Response body ResponseBody okBody = createOkBody(urlConnection); okResponseBuilder.body(okBody); // Handle SSL handshake information as needed. if (httpUrlConnection instanceof HttpsURLConnection) { HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) httpUrlConnection; Certificate[] peerCertificates; try { peerCertificates = httpsUrlConnection.getServerCertificates(); } catch (SSLPeerUnverifiedException e) { peerCertificates = null; } Certificate[] localCertificates = httpsUrlConnection.getLocalCertificates(); String cipherSuiteString = httpsUrlConnection.getCipherSuite(); CipherSuite cipherSuite = CipherSuite.forJavaName(cipherSuiteString); Handshake handshake = Handshake.get(TlsVersion.SSL_3_0, cipherSuite, nullSafeImmutableList(peerCertificates), nullSafeImmutableList(localCertificates)); okResponseBuilder.handshake(handshake); } return okResponseBuilder.build(); } /** * Returns headers for the header names and values in the {@link Map}. */ private static Headers createHeaders(Map<String, List<String>> headers) { Headers.Builder builder = new Headers.Builder(); for (Map.Entry<String, List<String>> header : headers.entrySet()) { if (header.getKey() == null || header.getValue() == null) { continue; } String name = header.getKey().trim(); for (String value : header.getValue()) { String trimmedValue = value.trim(); Internal.instance.addLenient(builder, name, trimmedValue); } } return builder.build(); } private static Headers varyHeaders(URLConnection urlConnection, Headers responseHeaders) { if (HttpHeaders.hasVaryAll(responseHeaders)) { // "*" means that this will be treated as uncacheable anyway. return null; } Set<String> varyFields = HttpHeaders.varyFields(responseHeaders); if (varyFields.isEmpty()) { return new Headers.Builder().build(); } // This probably indicates another HTTP stack is trying to use the shared ResponseCache. // We cannot guarantee this case will work properly because we cannot reliably extract *all* // the request header values, and we can't get multiple Vary request header values. // We also can't be sure about the Accept-Encoding behavior of other stacks. if (!(urlConnection instanceof CacheHttpURLConnection || urlConnection instanceof CacheHttpsURLConnection)) { return null; } // This is the case we expect: The URLConnection is from a call to // JavaApiConverter.createJavaUrlConnection() and we have access to the user's request headers. Map<String, List<String>> requestProperties = urlConnection.getRequestProperties(); Headers.Builder result = new Headers.Builder(); for (String fieldName : varyFields) { List<String> fieldValues = requestProperties.get(fieldName); if (fieldValues == null) { if (fieldName.equals("Accept-Encoding")) { // Accept-Encoding is special. If OkHttp sees Accept-Encoding is unset it will add // "gzip". We don't have access to the request that was actually made so we must do the // same. result.add("Accept-Encoding", "gzip"); } } else { for (String fieldValue : fieldValues) { Internal.instance.addLenient(result, fieldName, fieldValue); } } } return result.build(); } /** * Creates an OkHttp {@link Response} using the supplied {@link Request} and {@link CacheResponse} * to supply the data. */ static Response createOkResponseForCacheGet(Request request, CacheResponse javaResponse) throws IOException { // Build a cache request for the response to use. Headers responseHeaders = createHeaders(javaResponse.getHeaders()); Headers varyHeaders; if (HttpHeaders.hasVaryAll(responseHeaders)) { // "*" means that this will be treated as uncacheable anyway. varyHeaders = new Headers.Builder().build(); } else { varyHeaders = HttpHeaders.varyHeaders(request.headers(), responseHeaders); } Request cacheRequest = new Request.Builder() .url(request.url()) .method(request.method(), null) .headers(varyHeaders) .build(); Response.Builder okResponseBuilder = new Response.Builder(); // Request: Use the cacheRequest we built. okResponseBuilder.request(cacheRequest); // Status line: Java has this as one of the headers. StatusLine statusLine = StatusLine.parse(extractStatusLine(javaResponse)); okResponseBuilder.protocol(statusLine.protocol); okResponseBuilder.code(statusLine.code); okResponseBuilder.message(statusLine.message); // Response headers Headers okHeaders = extractOkHeaders(javaResponse, okResponseBuilder); okResponseBuilder.headers(okHeaders); // Response body ResponseBody okBody = createOkBody(okHeaders, javaResponse); okResponseBuilder.body(okBody); // Handle SSL handshake information as needed. if (javaResponse instanceof SecureCacheResponse) { SecureCacheResponse javaSecureCacheResponse = (SecureCacheResponse) javaResponse; // Handshake doesn't support null lists. List<Certificate> peerCertificates; try { peerCertificates = javaSecureCacheResponse.getServerCertificateChain(); } catch (SSLPeerUnverifiedException e) { peerCertificates = Collections.emptyList(); } List<Certificate> localCertificates = javaSecureCacheResponse.getLocalCertificateChain(); if (localCertificates == null) { localCertificates = Collections.emptyList(); } String cipherSuiteString = javaSecureCacheResponse.getCipherSuite(); CipherSuite cipherSuite = CipherSuite.forJavaName(cipherSuiteString); Handshake handshake = Handshake.get( TlsVersion.SSL_3_0, cipherSuite, peerCertificates, localCertificates); okResponseBuilder.handshake(handshake); } return okResponseBuilder.build(); } /** * Creates an OkHttp {@link Request} from the supplied information. * * <p>This method allows a {@code null} value for {@code requestHeaders} for situations where a * connection is already connected and access to the headers has been lost. See {@link * java.net.HttpURLConnection#getRequestProperties()} for details. */ public static Request createOkRequest( URI uri, String requestMethod, Map<String, List<String>> requestHeaders) { // OkHttp's Call API requires a placeholder body; the real body will be streamed separately. RequestBody placeholderBody = HttpMethod.requiresRequestBody(requestMethod) ? Util.EMPTY_REQUEST : null; Request.Builder builder = new Request.Builder() .url(uri.toString()) .method(requestMethod, placeholderBody); if (requestHeaders != null) { Headers headers = extractOkHeaders(requestHeaders, null); builder.headers(headers); } return builder.build(); } /** * Creates a {@link java.net.CacheResponse} of the correct (sub)type using information gathered * from the supplied {@link Response}. */ public static CacheResponse createJavaCacheResponse(final Response response) { final Headers headers = withSyntheticHeaders(response); final ResponseBody body = response.body(); if (response.request().isHttps()) { final Handshake handshake = response.handshake(); return new SecureCacheResponse() { @Override public String getCipherSuite() { return handshake != null ? handshake.cipherSuite().javaName() : null; } @Override public List<Certificate> getLocalCertificateChain() { if (handshake == null) return null; // Java requires null, not an empty list here. List<Certificate> certificates = handshake.localCertificates(); return certificates.size() > 0 ? certificates : null; } @Override public List<Certificate> getServerCertificateChain() throws SSLPeerUnverifiedException { if (handshake == null) return null; // Java requires null, not an empty list here. List<Certificate> certificates = handshake.peerCertificates(); return certificates.size() > 0 ? certificates : null; } @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { if (handshake == null) return null; return handshake.peerPrincipal(); } @Override public Principal getLocalPrincipal() { if (handshake == null) return null; return handshake.localPrincipal(); } @Override public Map<String, List<String>> getHeaders() throws IOException { // Java requires that the entry with a null key be the status line. return JavaNetHeaders.toMultimap(headers, StatusLine.get(response).toString()); } @Override public InputStream getBody() throws IOException { if (body == null) return null; return body.byteStream(); } }; } else { return new CacheResponse() { @Override public Map<String, List<String>> getHeaders() throws IOException { // Java requires that the entry with a null key be the status line. return JavaNetHeaders.toMultimap(headers, StatusLine.get(response).toString()); } @Override public InputStream getBody() throws IOException { if (body == null) return null; return body.byteStream(); } }; } } public static java.net.CacheRequest createJavaCacheRequest(final CacheRequest okCacheRequest) { return new java.net.CacheRequest() { @Override public void abort() { okCacheRequest.abort(); } @Override public OutputStream getBody() throws IOException { Sink body = okCacheRequest.body(); if (body == null) { return null; } return Okio.buffer(body).outputStream(); } }; } /** * Creates an {@link java.net.HttpURLConnection} of the correct subclass from the supplied OkHttp * {@link Response}. */ static HttpURLConnection createJavaUrlConnectionForCachePut(Response okResponse) { okResponse = okResponse.newBuilder() .body(null) .headers(withSyntheticHeaders(okResponse)) .build(); Request request = okResponse.request(); // Create an object of the correct class in case the ResponseCache uses instanceof. if (request.isHttps()) { return new CacheHttpsURLConnection(new CacheHttpURLConnection(okResponse)); } else { return new CacheHttpURLConnection(okResponse); } } private static Headers withSyntheticHeaders(Response okResponse) { return okResponse.headers().newBuilder() .add(SENT_MILLIS, Long.toString(okResponse.sentRequestAtMillis())) .add(RECEIVED_MILLIS, Long.toString(okResponse.receivedResponseAtMillis())) .build(); } /** * Extracts an immutable request header map from the supplied {@link Headers}. */ static Map<String, List<String>> extractJavaHeaders(Request request) { return JavaNetHeaders.toMultimap(request.headers(), null); } /** * Extracts OkHttp headers from the supplied {@link java.net.CacheResponse}. Only real headers are * extracted. See {@link #extractStatusLine(java.net.CacheResponse)}. */ private static Headers extractOkHeaders( CacheResponse javaResponse, Response.Builder okResponseBuilder) throws IOException { Map<String, List<String>> javaResponseHeaders = javaResponse.getHeaders(); return extractOkHeaders(javaResponseHeaders, okResponseBuilder); } /** * Extracts OkHttp headers from the supplied {@link java.net.HttpURLConnection}. Only real headers * are extracted. See {@link #extractStatusLine(java.net.HttpURLConnection)}. */ private static Headers extractOkResponseHeaders( HttpURLConnection httpUrlConnection, Response.Builder okResponseBuilder) { Map<String, List<String>> javaResponseHeaders = httpUrlConnection.getHeaderFields(); return extractOkHeaders(javaResponseHeaders, okResponseBuilder); } /** * Extracts OkHttp headers from the supplied {@link Map}. Only real headers are extracted. Any * entry (one with a {@code null} key) is discarded. Special internal headers used to track cache * metadata are omitted from the result and added to {@code okResponseBuilder} instead. */ // @VisibleForTesting static Headers extractOkHeaders( Map<String, List<String>> javaHeaders, Response.Builder okResponseBuilder) { Headers.Builder okHeadersBuilder = new Headers.Builder(); for (Map.Entry<String, List<String>> javaHeader : javaHeaders.entrySet()) { String name = javaHeader.getKey(); if (name == null) { // The Java API uses the null key to store the status line in responses. // Earlier versions of OkHttp would use the null key to store the "request line" in // requests. e.g. "GET / HTTP 1.1". Although this is no longer the case it must be // explicitly ignored because Headers.Builder does not support null keys. continue; } if (okResponseBuilder != null && javaHeader.getValue().size() == 1) { if (name.equals(SENT_MILLIS)) { okResponseBuilder.sentRequestAtMillis(Long.valueOf(javaHeader.getValue().get(0))); continue; } if (name.equals(RECEIVED_MILLIS)) { okResponseBuilder.receivedResponseAtMillis(Long.valueOf(javaHeader.getValue().get(0))); continue; } } for (String value : javaHeader.getValue()) { Internal.instance.addLenient(okHeadersBuilder, name, value); } } return okHeadersBuilder.build(); } /** * Extracts the status line from the supplied Java API {@link java.net.HttpURLConnection}. As per * the spec, the status line is held as the header with the null key. Returns {@code null} if * there is no status line. */ private static String extractStatusLine(HttpURLConnection httpUrlConnection) { // Java specifies that this will be be response header with a null key. return httpUrlConnection.getHeaderField(null); } /** * Extracts the status line from the supplied Java API {@link java.net.CacheResponse}. As per the * spec, the status line is held as the header with the null key. Throws a {@link * ProtocolException} if there is no status line. */ private static String extractStatusLine(CacheResponse javaResponse) throws IOException { Map<String, List<String>> javaResponseHeaders = javaResponse.getHeaders(); return extractStatusLine(javaResponseHeaders); } // VisibleForTesting static String extractStatusLine(Map<String, List<String>> javaResponseHeaders) throws ProtocolException { List<String> values = javaResponseHeaders.get(null); if (values == null || values.size() == 0) { // The status line is missing. This suggests a badly behaving cache. throw new ProtocolException( "CacheResponse is missing a \'null\' header containing the status line. Headers=" + javaResponseHeaders); } return values.get(0); } /** * Creates an OkHttp Response.Body containing the supplied information. */ private static ResponseBody createOkBody(final Headers okHeaders, final CacheResponse cacheResponse) throws IOException { final BufferedSource body = Okio.buffer(Okio.source(cacheResponse.getBody())); return new ResponseBody() { @Override public MediaType contentType() { String contentTypeHeader = okHeaders.get("Content-Type"); return contentTypeHeader == null ? null : MediaType.parse(contentTypeHeader); } @Override public long contentLength() { return HttpHeaders.contentLength(okHeaders); } @Override public BufferedSource source() { return body; } }; } /** * Creates an OkHttp Response.Body containing the supplied information. */ private static ResponseBody createOkBody(final URLConnection urlConnection) throws IOException { if (!urlConnection.getDoInput()) { return null; } final BufferedSource body = Okio.buffer(Okio.source(urlConnection.getInputStream())); return new ResponseBody() { @Override public MediaType contentType() { String contentTypeHeader = urlConnection.getContentType(); return contentTypeHeader == null ? null : MediaType.parse(contentTypeHeader); } @Override public long contentLength() { String s = urlConnection.getHeaderField("Content-Length"); return stringToLong(s); } @Override public BufferedSource source() { return body; } }; } /** * An {@link java.net.HttpURLConnection} that represents an HTTP request at the point where the * request has been made, and the response headers have been received, but the body content, if * present, has not been read yet. This intended to provide enough information for {@link * java.net.ResponseCache} subclasses and no more. * * <p>Much of the method implementations are overrides to delegate to the OkHttp request and * response, or to deny access to information as a real HttpURLConnection would after connection. */ private static final class CacheHttpURLConnection extends HttpURLConnection { private final Request request; private final Response response; CacheHttpURLConnection(Response response) { super(response.request().url().url()); this.request = response.request(); this.response = response; // Configure URLConnection inherited fields. this.connected = true; this.doOutput = request.body() != null; this.doInput = true; this.useCaches = true; // Configure HttpUrlConnection inherited fields. this.method = request.method(); } // HTTP connection lifecycle methods @Override public void connect() throws IOException { throw throwRequestModificationException(); } @Override public void disconnect() { throw throwRequestModificationException(); } // HTTP Request methods @Override public void setRequestProperty(String key, String value) { throw throwRequestModificationException(); } @Override public void addRequestProperty(String key, String value) { throw throwRequestModificationException(); } @Override public String getRequestProperty(String key) { return request.header(key); } @Override public Map<String, List<String>> getRequestProperties() { // The RI and OkHttp's HttpURLConnectionImpl fail this call after connect() as required by the // spec. There seems no good reason why this should fail while getRequestProperty() is ok. // We don't fail here, because we need all request header values for caching Vary responses // correctly. return JavaNetHeaders.toMultimap(request.headers(), null); } @Override public void setFixedLengthStreamingMode(int contentLength) { throw throwRequestModificationException(); } @Override public void setFixedLengthStreamingMode(long contentLength) { throw throwRequestModificationException(); } @Override public void setChunkedStreamingMode(int chunklen) { throw throwRequestModificationException(); } @Override public void setInstanceFollowRedirects(boolean followRedirects) { throw throwRequestModificationException(); } @Override public boolean getInstanceFollowRedirects() { // Return the platform default. return super.getInstanceFollowRedirects(); } @Override public void setRequestMethod(String method) throws ProtocolException { throw throwRequestModificationException(); } @Override public String getRequestMethod() { return request.method(); } // HTTP Response methods @Override public String getHeaderFieldKey(int position) { // Deal with index 0 meaning "status line" if (position < 0) { throw new IllegalArgumentException("Invalid header index: " + position); } if (position == 0 || position > response.headers().size()) { return null; } return response.headers().name(position - 1); } @Override public String getHeaderField(int position) { // Deal with index 0 meaning "status line" if (position < 0) { throw new IllegalArgumentException("Invalid header index: " + position); } if (position == 0) { return StatusLine.get(response).toString(); } if (position > response.headers().size()) { return null; } return response.headers().value(position - 1); } @Override public String getHeaderField(String fieldName) { return fieldName == null ? StatusLine.get(response).toString() : response.headers().get(fieldName); } @Override public Map<String, List<String>> getHeaderFields() { return JavaNetHeaders.toMultimap(response.headers(), StatusLine.get(response).toString()); } @Override public int getResponseCode() throws IOException { return response.code(); } @Override public String getResponseMessage() throws IOException { return response.message(); } @Override public InputStream getErrorStream() { return null; } // HTTP miscellaneous methods @Override public boolean usingProxy() { // It's safe to return false here, even if a proxy is in use. The problem is we don't // necessarily know if we're going to use a proxy by the time we ask the cache for a response. return false; } // URLConnection methods @Override public void setConnectTimeout(int timeout) { throw throwRequestModificationException(); } @Override public int getConnectTimeout() { // Impossible to say. return 0; } @Override public void setReadTimeout(int timeout) { throw throwRequestModificationException(); } @Override public int getReadTimeout() { // Impossible to say. return 0; } @Override public Object getContent() throws IOException { throw throwResponseBodyAccessException(); } @Override public Object getContent(Class[] classes) throws IOException { throw throwResponseBodyAccessException(); } @Override public InputStream getInputStream() throws IOException { return new InputStream() { @Override public int read() throws IOException { throw throwResponseBodyAccessException(); } }; } @Override public OutputStream getOutputStream() throws IOException { throw throwRequestModificationException(); } @Override public void setDoInput(boolean doInput) { throw throwRequestModificationException(); } @Override public boolean getDoInput() { return doInput; } @Override public void setDoOutput(boolean doOutput) { throw throwRequestModificationException(); } @Override public boolean getDoOutput() { return doOutput; } @Override public void setAllowUserInteraction(boolean allowUserInteraction) { throw throwRequestModificationException(); } @Override public boolean getAllowUserInteraction() { return false; } @Override public void setUseCaches(boolean useCaches) { throw throwRequestModificationException(); } @Override public boolean getUseCaches() { return super.getUseCaches(); } @Override public void setIfModifiedSince(long ifModifiedSince) { throw throwRequestModificationException(); } @Override public long getIfModifiedSince() { return stringToLong(request.headers().get("If-Modified-Since")); } @Override public boolean getDefaultUseCaches() { return super.getDefaultUseCaches(); } @Override public void setDefaultUseCaches(boolean defaultUseCaches) { super.setDefaultUseCaches(defaultUseCaches); } } /** An HttpsURLConnection to offer to the cache. */ private static final class CacheHttpsURLConnection extends DelegatingHttpsURLConnection { private final CacheHttpURLConnection delegate; CacheHttpsURLConnection(CacheHttpURLConnection delegate) { super(delegate); this.delegate = delegate; } @Override protected Handshake handshake() { return delegate.response.handshake(); } @Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { throw throwRequestModificationException(); } @Override public HostnameVerifier getHostnameVerifier() { throw throwRequestSslAccessException(); } @Override public void setSSLSocketFactory(SSLSocketFactory socketFactory) { throw throwRequestModificationException(); } @Override public SSLSocketFactory getSSLSocketFactory() { throw throwRequestSslAccessException(); } } private static RuntimeException throwRequestModificationException() { throw new UnsupportedOperationException("ResponseCache cannot modify the request."); } private static RuntimeException throwRequestHeaderAccessException() { throw new UnsupportedOperationException("ResponseCache cannot access request headers"); } private static RuntimeException throwRequestSslAccessException() { throw new UnsupportedOperationException("ResponseCache cannot access SSL internals"); } private static RuntimeException throwResponseBodyAccessException() { throw new UnsupportedOperationException("ResponseCache cannot access the response body."); } private static <T> List<T> nullSafeImmutableList(T[] elements) { return elements == null ? Collections.<T>emptyList() : Util.immutableList(elements); } private static long stringToLong(String s) { if (s == null) return -1; try { return Long.parseLong(s); } catch (NumberFormatException e) { return -1; } } }