blob: 026c45f38bc80d22fcbe7466ede815cc35e094f1 [file] [log] [blame]
/*
* 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 com.squareup.okhttp.internal.huc;
import com.squareup.okhttp.Handshake;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.http.CacheRequest;
import com.squareup.okhttp.internal.http.HttpMethod;
import com.squareup.okhttp.internal.http.OkHeaders;
import com.squareup.okhttp.internal.http.StatusLine;
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 okio.BufferedSource;
import okio.Okio;
import okio.Sink;
/**
* Helper methods that convert between Java and OkHttp representations.
*/
public final class JavaApiConverter {
private static final RequestBody EMPTY_REQUEST_BODY = RequestBody.create(null, new byte[0]);
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)
? EMPTY_REQUEST_BODY
: 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.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();
Handshake handshake = Handshake.get(
httpsUrlConnection.getCipherSuite(), 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 (OkHeaders.hasVaryAll(responseHeaders)) {
// "*" means that this will be treated as uncacheable anyway.
return null;
}
Set<String> varyFields = OkHeaders.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 (OkHeaders.hasVaryAll(responseHeaders)) {
// "*" means that this will be treated as uncacheable anyway.
varyHeaders = new Headers.Builder().build();
} else {
varyHeaders = OkHeaders.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.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();
}
Handshake handshake = Handshake.get(
javaSecureCacheResponse.getCipherSuite(), 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)
? EMPTY_REQUEST_BODY
: null;
Request.Builder builder = new Request.Builder()
.url(uri.toString())
.method(requestMethod, placeholderBody);
if (requestHeaders != null) {
Headers headers = extractOkHeaders(requestHeaders);
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 = response.headers();
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() : 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 OkHeaders.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 OkHeaders.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) {
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);
}
}
/**
* Extracts an immutable request header map from the supplied {@link com.squareup.okhttp.Headers}.
*/
static Map<String, List<String>> extractJavaHeaders(Request request) {
return OkHeaders.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) throws IOException {
Map<String, List<String>> javaResponseHeaders = javaResponse.getHeaders();
return extractOkHeaders(javaResponseHeaders);
}
/**
* 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) {
Map<String, List<String>> javaResponseHeaders = httpUrlConnection.getHeaderFields();
return extractOkHeaders(javaResponseHeaders);
}
/**
* Extracts OkHttp headers from the supplied {@link Map}. Only real headers are
* extracted. Any entry (one with a {@code null} key) is discarded.
*/
// @VisibleForTesting
static Headers extractOkHeaders(Map<String, List<String>> javaHeaders) {
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;
}
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) {
return new ResponseBody() {
private BufferedSource body;
@Override
public MediaType contentType() {
String contentTypeHeader = okHeaders.get("Content-Type");
return contentTypeHeader == null ? null : MediaType.parse(contentTypeHeader);
}
@Override
public long contentLength() {
return OkHeaders.contentLength(okHeaders);
}
@Override public BufferedSource source() throws IOException {
if (body == null) {
InputStream is = cacheResponse.getBody();
body = Okio.buffer(Okio.source(is));
}
return body;
}
};
}
/**
* Creates an OkHttp Response.Body containing the supplied information.
*/
private static ResponseBody createOkBody(final URLConnection urlConnection) {
if (!urlConnection.getDoInput()) {
return null;
}
return new ResponseBody() {
private BufferedSource body;
@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() throws IOException {
if (body == null) {
InputStream is = urlConnection.getInputStream();
body = Okio.buffer(Okio.source(is));
}
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;
public CacheHttpURLConnection(Response response) {
super(response.request().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 OkHeaders.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) {
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();
}
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 OkHeaders.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 {
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;
public 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();
}
// ANDROID-BEGIN
// @Override public long getContentLengthLong() {
// return delegate.getContentLengthLong();
// }
// ANDROID-END
@Override public void setFixedLengthStreamingMode(long contentLength) {
delegate.setFixedLengthStreamingMode(contentLength);
}
// ANDROID-BEGIN
// @Override public long getHeaderFieldLong(String field, long defaultValue) {
// return delegate.getHeaderFieldLong(field, defaultValue);
// }
// ANDROID-END
}
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;
}
}
}