Home | History | Annotate | Download | only in huc
      1 /*
      2  * Copyright (C) 2014 Square, Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.squareup.okhttp.internal.huc;
     17 
     18 import com.squareup.okhttp.Handshake;
     19 import com.squareup.okhttp.Headers;
     20 import com.squareup.okhttp.MediaType;
     21 import com.squareup.okhttp.Request;
     22 import com.squareup.okhttp.RequestBody;
     23 import com.squareup.okhttp.Response;
     24 import com.squareup.okhttp.ResponseBody;
     25 import com.squareup.okhttp.internal.Internal;
     26 import com.squareup.okhttp.internal.Util;
     27 import com.squareup.okhttp.internal.http.CacheRequest;
     28 import com.squareup.okhttp.internal.http.HttpMethod;
     29 import com.squareup.okhttp.internal.http.OkHeaders;
     30 import com.squareup.okhttp.internal.http.StatusLine;
     31 import java.io.IOException;
     32 import java.io.InputStream;
     33 import java.io.OutputStream;
     34 import java.net.CacheResponse;
     35 import java.net.HttpURLConnection;
     36 import java.net.ProtocolException;
     37 import java.net.SecureCacheResponse;
     38 import java.net.URI;
     39 import java.net.URLConnection;
     40 import java.security.Principal;
     41 import java.security.cert.Certificate;
     42 import java.util.Collections;
     43 import java.util.List;
     44 import java.util.Map;
     45 import java.util.Set;
     46 import javax.net.ssl.HostnameVerifier;
     47 import javax.net.ssl.HttpsURLConnection;
     48 import javax.net.ssl.SSLPeerUnverifiedException;
     49 import javax.net.ssl.SSLSocketFactory;
     50 import okio.BufferedSource;
     51 import okio.Okio;
     52 import okio.Sink;
     53 
     54 /**
     55  * Helper methods that convert between Java and OkHttp representations.
     56  */
     57 public final class JavaApiConverter {
     58   private static final RequestBody EMPTY_REQUEST_BODY = RequestBody.create(null, new byte[0]);
     59 
     60   private JavaApiConverter() {
     61   }
     62 
     63   /**
     64    * Creates an OkHttp {@link Response} using the supplied {@link URI} and {@link URLConnection}
     65    * to supply the data. The URLConnection is assumed to already be connected. If this method
     66    * returns {@code null} the response is uncacheable.
     67    */
     68   public static Response createOkResponseForCachePut(URI uri, URLConnection urlConnection)
     69       throws IOException {
     70 
     71     HttpURLConnection httpUrlConnection = (HttpURLConnection) urlConnection;
     72 
     73     Response.Builder okResponseBuilder = new Response.Builder();
     74 
     75     // Request: Create one from the URL connection.
     76     Headers responseHeaders = createHeaders(urlConnection.getHeaderFields());
     77     // Some request headers are needed for Vary caching.
     78     Headers varyHeaders = varyHeaders(urlConnection, responseHeaders);
     79     if (varyHeaders == null) {
     80       return null;
     81     }
     82 
     83     // OkHttp's Call API requires a placeholder body; the real body will be streamed separately.
     84     String requestMethod = httpUrlConnection.getRequestMethod();
     85     RequestBody placeholderBody = HttpMethod.requiresRequestBody(requestMethod)
     86         ? EMPTY_REQUEST_BODY
     87         : null;
     88 
     89     Request okRequest = new Request.Builder()
     90         .url(uri.toString())
     91         .method(requestMethod, placeholderBody)
     92         .headers(varyHeaders)
     93         .build();
     94     okResponseBuilder.request(okRequest);
     95 
     96     // Status line
     97     StatusLine statusLine = StatusLine.parse(extractStatusLine(httpUrlConnection));
     98     okResponseBuilder.protocol(statusLine.protocol);
     99     okResponseBuilder.code(statusLine.code);
    100     okResponseBuilder.message(statusLine.message);
    101 
    102     // A network response is required for the Cache to find any Vary headers it needs.
    103     Response networkResponse = okResponseBuilder.build();
    104     okResponseBuilder.networkResponse(networkResponse);
    105 
    106     // Response headers
    107     Headers okHeaders = extractOkResponseHeaders(httpUrlConnection);
    108     okResponseBuilder.headers(okHeaders);
    109 
    110     // Response body
    111     ResponseBody okBody = createOkBody(urlConnection);
    112     okResponseBuilder.body(okBody);
    113 
    114     // Handle SSL handshake information as needed.
    115     if (httpUrlConnection instanceof HttpsURLConnection) {
    116       HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) httpUrlConnection;
    117 
    118       Certificate[] peerCertificates;
    119       try {
    120         peerCertificates = httpsUrlConnection.getServerCertificates();
    121       } catch (SSLPeerUnverifiedException e) {
    122         peerCertificates = null;
    123       }
    124 
    125       Certificate[] localCertificates = httpsUrlConnection.getLocalCertificates();
    126 
    127       Handshake handshake = Handshake.get(
    128           httpsUrlConnection.getCipherSuite(), nullSafeImmutableList(peerCertificates),
    129           nullSafeImmutableList(localCertificates));
    130       okResponseBuilder.handshake(handshake);
    131     }
    132 
    133     return okResponseBuilder.build();
    134   }
    135 
    136   /**
    137    * Returns headers for the header names and values in the {@link Map}.
    138    */
    139   private static Headers createHeaders(Map<String, List<String>> headers) {
    140     Headers.Builder builder = new Headers.Builder();
    141     for (Map.Entry<String, List<String>> header : headers.entrySet()) {
    142       if (header.getKey() == null || header.getValue() == null) {
    143         continue;
    144       }
    145       String name = header.getKey().trim();
    146       for (String value : header.getValue()) {
    147         String trimmedValue = value.trim();
    148         Internal.instance.addLenient(builder, name, trimmedValue);
    149       }
    150     }
    151     return builder.build();
    152   }
    153 
    154   private static Headers varyHeaders(URLConnection urlConnection, Headers responseHeaders) {
    155     if (OkHeaders.hasVaryAll(responseHeaders)) {
    156       // "*" means that this will be treated as uncacheable anyway.
    157       return null;
    158     }
    159     Set<String> varyFields = OkHeaders.varyFields(responseHeaders);
    160     if (varyFields.isEmpty()) {
    161       return new Headers.Builder().build();
    162     }
    163 
    164     // This probably indicates another HTTP stack is trying to use the shared ResponseCache.
    165     // We cannot guarantee this case will work properly because we cannot reliably extract *all*
    166     // the request header values, and we can't get multiple Vary request header values.
    167     // We also can't be sure about the Accept-Encoding behavior of other stacks.
    168     if (!(urlConnection instanceof CacheHttpURLConnection
    169         || urlConnection instanceof CacheHttpsURLConnection)) {
    170       return null;
    171     }
    172 
    173     // This is the case we expect: The URLConnection is from a call to
    174     // JavaApiConverter.createJavaUrlConnection() and we have access to the user's request headers.
    175     Map<String, List<String>> requestProperties = urlConnection.getRequestProperties();
    176     Headers.Builder result = new Headers.Builder();
    177     for (String fieldName : varyFields) {
    178       List<String> fieldValues = requestProperties.get(fieldName);
    179       if (fieldValues == null) {
    180         if (fieldName.equals("Accept-Encoding")) {
    181           // Accept-Encoding is special. If OkHttp sees Accept-Encoding is unset it will add
    182           // "gzip". We don't have access to the request that was actually made so we must do the
    183           // same.
    184           result.add("Accept-Encoding", "gzip");
    185         }
    186       } else {
    187         for (String fieldValue : fieldValues) {
    188           Internal.instance.addLenient(result, fieldName, fieldValue);
    189         }
    190       }
    191     }
    192     return result.build();
    193   }
    194 
    195   /**
    196    * Creates an OkHttp {@link Response} using the supplied {@link Request} and {@link CacheResponse}
    197    * to supply the data.
    198    */
    199   static Response createOkResponseForCacheGet(Request request, CacheResponse javaResponse)
    200       throws IOException {
    201 
    202     // Build a cache request for the response to use.
    203     Headers responseHeaders = createHeaders(javaResponse.getHeaders());
    204     Headers varyHeaders;
    205     if (OkHeaders.hasVaryAll(responseHeaders)) {
    206       // "*" means that this will be treated as uncacheable anyway.
    207       varyHeaders = new Headers.Builder().build();
    208     } else {
    209       varyHeaders = OkHeaders.varyHeaders(request.headers(), responseHeaders);
    210     }
    211 
    212     Request cacheRequest = new Request.Builder()
    213         .url(request.httpUrl())
    214         .method(request.method(), null)
    215         .headers(varyHeaders)
    216         .build();
    217 
    218     Response.Builder okResponseBuilder = new Response.Builder();
    219 
    220     // Request: Use the cacheRequest we built.
    221     okResponseBuilder.request(cacheRequest);
    222 
    223     // Status line: Java has this as one of the headers.
    224     StatusLine statusLine = StatusLine.parse(extractStatusLine(javaResponse));
    225     okResponseBuilder.protocol(statusLine.protocol);
    226     okResponseBuilder.code(statusLine.code);
    227     okResponseBuilder.message(statusLine.message);
    228 
    229     // Response headers
    230     Headers okHeaders = extractOkHeaders(javaResponse);
    231     okResponseBuilder.headers(okHeaders);
    232 
    233     // Response body
    234     ResponseBody okBody = createOkBody(okHeaders, javaResponse);
    235     okResponseBuilder.body(okBody);
    236 
    237     // Handle SSL handshake information as needed.
    238     if (javaResponse instanceof SecureCacheResponse) {
    239       SecureCacheResponse javaSecureCacheResponse = (SecureCacheResponse) javaResponse;
    240 
    241       // Handshake doesn't support null lists.
    242       List<Certificate> peerCertificates;
    243       try {
    244         peerCertificates = javaSecureCacheResponse.getServerCertificateChain();
    245       } catch (SSLPeerUnverifiedException e) {
    246         peerCertificates = Collections.emptyList();
    247       }
    248       List<Certificate> localCertificates = javaSecureCacheResponse.getLocalCertificateChain();
    249       if (localCertificates == null) {
    250         localCertificates = Collections.emptyList();
    251       }
    252       Handshake handshake = Handshake.get(
    253           javaSecureCacheResponse.getCipherSuite(), peerCertificates, localCertificates);
    254       okResponseBuilder.handshake(handshake);
    255     }
    256 
    257     return okResponseBuilder.build();
    258   }
    259 
    260   /**
    261    * Creates an OkHttp {@link Request} from the supplied information.
    262    *
    263    * <p>This method allows a {@code null} value for {@code requestHeaders} for situations
    264    * where a connection is already connected and access to the headers has been lost.
    265    * See {@link java.net.HttpURLConnection#getRequestProperties()} for details.
    266    */
    267   public static Request createOkRequest(
    268       URI uri, String requestMethod, Map<String, List<String>> requestHeaders) {
    269     // OkHttp's Call API requires a placeholder body; the real body will be streamed separately.
    270     RequestBody placeholderBody = HttpMethod.requiresRequestBody(requestMethod)
    271         ? EMPTY_REQUEST_BODY
    272         : null;
    273 
    274     Request.Builder builder = new Request.Builder()
    275         .url(uri.toString())
    276         .method(requestMethod, placeholderBody);
    277 
    278     if (requestHeaders != null) {
    279       Headers headers = extractOkHeaders(requestHeaders);
    280       builder.headers(headers);
    281     }
    282     return builder.build();
    283   }
    284 
    285   /**
    286    * Creates a {@link java.net.CacheResponse} of the correct (sub)type using information
    287    * gathered from the supplied {@link Response}.
    288    */
    289   public static CacheResponse createJavaCacheResponse(final Response response) {
    290     final Headers headers = response.headers();
    291     final ResponseBody body = response.body();
    292     if (response.request().isHttps()) {
    293       final Handshake handshake = response.handshake();
    294       return new SecureCacheResponse() {
    295         @Override
    296         public String getCipherSuite() {
    297           return handshake != null ? handshake.cipherSuite() : null;
    298         }
    299 
    300         @Override
    301         public List<Certificate> getLocalCertificateChain() {
    302           if (handshake == null) return null;
    303           // Java requires null, not an empty list here.
    304           List<Certificate> certificates = handshake.localCertificates();
    305           return certificates.size() > 0 ? certificates : null;
    306         }
    307 
    308         @Override
    309         public List<Certificate> getServerCertificateChain() throws SSLPeerUnverifiedException {
    310           if (handshake == null) return null;
    311           // Java requires null, not an empty list here.
    312           List<Certificate> certificates = handshake.peerCertificates();
    313           return certificates.size() > 0 ? certificates : null;
    314         }
    315 
    316         @Override
    317         public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
    318           if (handshake == null) return null;
    319           return handshake.peerPrincipal();
    320         }
    321 
    322         @Override
    323         public Principal getLocalPrincipal() {
    324           if (handshake == null) return null;
    325           return handshake.localPrincipal();
    326         }
    327 
    328         @Override
    329         public Map<String, List<String>> getHeaders() throws IOException {
    330           // Java requires that the entry with a null key be the status line.
    331           return OkHeaders.toMultimap(headers, StatusLine.get(response).toString());
    332         }
    333 
    334         @Override
    335         public InputStream getBody() throws IOException {
    336           if (body == null) return null;
    337           return body.byteStream();
    338         }
    339       };
    340     } else {
    341       return new CacheResponse() {
    342         @Override
    343         public Map<String, List<String>> getHeaders() throws IOException {
    344           // Java requires that the entry with a null key be the status line.
    345           return OkHeaders.toMultimap(headers, StatusLine.get(response).toString());
    346         }
    347 
    348         @Override
    349         public InputStream getBody() throws IOException {
    350           if (body == null) return null;
    351           return body.byteStream();
    352         }
    353       };
    354     }
    355   }
    356 
    357   public static java.net.CacheRequest createJavaCacheRequest(final CacheRequest okCacheRequest) {
    358     return new java.net.CacheRequest() {
    359       @Override
    360       public void abort() {
    361         okCacheRequest.abort();
    362       }
    363       @Override
    364       public OutputStream getBody() throws IOException {
    365         Sink body = okCacheRequest.body();
    366         if (body == null) {
    367           return null;
    368         }
    369         return Okio.buffer(body).outputStream();
    370       }
    371     };
    372   }
    373 
    374   /**
    375    * Creates an {@link java.net.HttpURLConnection} of the correct subclass from the supplied OkHttp
    376    * {@link Response}.
    377    */
    378   static HttpURLConnection createJavaUrlConnectionForCachePut(Response okResponse) {
    379     Request request = okResponse.request();
    380     // Create an object of the correct class in case the ResponseCache uses instanceof.
    381     if (request.isHttps()) {
    382       return new CacheHttpsURLConnection(new CacheHttpURLConnection(okResponse));
    383     } else {
    384       return new CacheHttpURLConnection(okResponse);
    385     }
    386   }
    387 
    388   /**
    389    * Extracts an immutable request header map from the supplied {@link com.squareup.okhttp.Headers}.
    390    */
    391   static Map<String, List<String>> extractJavaHeaders(Request request) {
    392     return OkHeaders.toMultimap(request.headers(), null);
    393   }
    394 
    395   /**
    396    * Extracts OkHttp headers from the supplied {@link java.net.CacheResponse}. Only real headers are
    397    * extracted. See {@link #extractStatusLine(java.net.CacheResponse)}.
    398    */
    399   private static Headers extractOkHeaders(CacheResponse javaResponse) throws IOException {
    400     Map<String, List<String>> javaResponseHeaders = javaResponse.getHeaders();
    401     return extractOkHeaders(javaResponseHeaders);
    402   }
    403 
    404   /**
    405    * Extracts OkHttp headers from the supplied {@link java.net.HttpURLConnection}. Only real headers
    406    * are extracted. See {@link #extractStatusLine(java.net.HttpURLConnection)}.
    407    */
    408   private static Headers extractOkResponseHeaders(HttpURLConnection httpUrlConnection) {
    409     Map<String, List<String>> javaResponseHeaders = httpUrlConnection.getHeaderFields();
    410     return extractOkHeaders(javaResponseHeaders);
    411   }
    412 
    413   /**
    414    * Extracts OkHttp headers from the supplied {@link Map}. Only real headers are
    415    * extracted. Any entry (one with a {@code null} key) is discarded.
    416    */
    417   // @VisibleForTesting
    418   static Headers extractOkHeaders(Map<String, List<String>> javaHeaders) {
    419     Headers.Builder okHeadersBuilder = new Headers.Builder();
    420     for (Map.Entry<String, List<String>> javaHeader : javaHeaders.entrySet()) {
    421       String name = javaHeader.getKey();
    422       if (name == null) {
    423         // The Java API uses the null key to store the status line in responses.
    424         // Earlier versions of OkHttp would use the null key to store the "request line" in
    425         // requests. e.g. "GET / HTTP 1.1". Although this is no longer the case it must be
    426         // explicitly ignored because Headers.Builder does not support null keys.
    427         continue;
    428       }
    429       for (String value : javaHeader.getValue()) {
    430         Internal.instance.addLenient(okHeadersBuilder, name, value);
    431       }
    432     }
    433     return okHeadersBuilder.build();
    434   }
    435 
    436   /**
    437    * Extracts the status line from the supplied Java API {@link java.net.HttpURLConnection}.
    438    * As per the spec, the status line is held as the header with the null key. Returns {@code null}
    439    * if there is no status line.
    440    */
    441   private static String extractStatusLine(HttpURLConnection httpUrlConnection) {
    442     // Java specifies that this will be be response header with a null key.
    443     return httpUrlConnection.getHeaderField(null);
    444   }
    445 
    446   /**
    447    * Extracts the status line from the supplied Java API {@link java.net.CacheResponse}.
    448    * As per the spec, the status line is held as the header with the null key. Throws a
    449    * {@link ProtocolException} if there is no status line.
    450    */
    451   private static String extractStatusLine(CacheResponse javaResponse) throws IOException {
    452     Map<String, List<String>> javaResponseHeaders = javaResponse.getHeaders();
    453     return extractStatusLine(javaResponseHeaders);
    454   }
    455 
    456   // VisibleForTesting
    457   static String extractStatusLine(Map<String, List<String>> javaResponseHeaders)
    458       throws ProtocolException {
    459     List<String> values = javaResponseHeaders.get(null);
    460     if (values == null || values.size() == 0) {
    461       // The status line is missing. This suggests a badly behaving cache.
    462       throw new ProtocolException(
    463           "CacheResponse is missing a \'null\' header containing the status line. Headers="
    464           + javaResponseHeaders);
    465     }
    466     return values.get(0);
    467   }
    468 
    469   /**
    470    * Creates an OkHttp Response.Body containing the supplied information.
    471    */
    472   private static ResponseBody createOkBody(final Headers okHeaders,
    473       final CacheResponse cacheResponse) {
    474     return new ResponseBody() {
    475       private BufferedSource body;
    476 
    477       @Override
    478       public MediaType contentType() {
    479         String contentTypeHeader = okHeaders.get("Content-Type");
    480         return contentTypeHeader == null ? null : MediaType.parse(contentTypeHeader);
    481       }
    482 
    483       @Override
    484       public long contentLength() {
    485         return OkHeaders.contentLength(okHeaders);
    486       }
    487       @Override public BufferedSource source() throws IOException {
    488         if (body == null) {
    489           InputStream is = cacheResponse.getBody();
    490           body = Okio.buffer(Okio.source(is));
    491         }
    492         return body;
    493       }
    494     };
    495   }
    496 
    497   /**
    498    * Creates an OkHttp Response.Body containing the supplied information.
    499    */
    500   private static ResponseBody createOkBody(final URLConnection urlConnection) {
    501     if (!urlConnection.getDoInput()) {
    502       return null;
    503     }
    504     return new ResponseBody() {
    505       private BufferedSource body;
    506 
    507       @Override public MediaType contentType() {
    508         String contentTypeHeader = urlConnection.getContentType();
    509         return contentTypeHeader == null ? null : MediaType.parse(contentTypeHeader);
    510       }
    511       @Override public long contentLength() {
    512         String s = urlConnection.getHeaderField("Content-Length");
    513         return stringToLong(s);
    514       }
    515       @Override public BufferedSource source() throws IOException {
    516         if (body == null) {
    517           InputStream is = urlConnection.getInputStream();
    518           body = Okio.buffer(Okio.source(is));
    519         }
    520         return body;
    521       }
    522     };
    523   }
    524 
    525   /**
    526    * An {@link java.net.HttpURLConnection} that represents an HTTP request at the point where
    527    * the request has been made, and the response headers have been received, but the body content,
    528    * if present, has not been read yet. This intended to provide enough information for
    529    * {@link java.net.ResponseCache} subclasses and no more.
    530    *
    531    * <p>Much of the method implementations are overrides to delegate to the OkHttp request and
    532    * response, or to deny access to information as a real HttpURLConnection would after connection.
    533    */
    534   private static final class CacheHttpURLConnection extends HttpURLConnection {
    535 
    536     private final Request request;
    537     private final Response response;
    538 
    539     public CacheHttpURLConnection(Response response) {
    540       super(response.request().url());
    541       this.request = response.request();
    542       this.response = response;
    543 
    544       // Configure URLConnection inherited fields.
    545       this.connected = true;
    546       this.doOutput = request.body() != null;
    547       this.doInput = true;
    548       this.useCaches = true;
    549 
    550       // Configure HttpUrlConnection inherited fields.
    551       this.method = request.method();
    552     }
    553 
    554     // HTTP connection lifecycle methods
    555 
    556     @Override
    557     public void connect() throws IOException {
    558       throw throwRequestModificationException();
    559     }
    560 
    561     @Override
    562     public void disconnect() {
    563       throw throwRequestModificationException();
    564     }
    565 
    566     // HTTP Request methods
    567 
    568     @Override
    569     public void setRequestProperty(String key, String value) {
    570       throw throwRequestModificationException();
    571     }
    572 
    573     @Override
    574     public void addRequestProperty(String key, String value) {
    575       throw throwRequestModificationException();
    576     }
    577 
    578     @Override
    579     public String getRequestProperty(String key) {
    580       return request.header(key);
    581     }
    582 
    583     @Override
    584     public Map<String, List<String>> getRequestProperties() {
    585       // The RI and OkHttp's HttpURLConnectionImpl fail this call after connect() as required by the
    586       // spec. There seems no good reason why this should fail while getRequestProperty() is ok.
    587       // We don't fail here, because we need all request header values for caching Vary responses
    588       // correctly.
    589       return OkHeaders.toMultimap(request.headers(), null);
    590     }
    591 
    592     @Override
    593     public void setFixedLengthStreamingMode(int contentLength) {
    594       throw throwRequestModificationException();
    595     }
    596 
    597     @Override
    598     public void setFixedLengthStreamingMode(long contentLength) {
    599       throw throwRequestModificationException();
    600     }
    601 
    602     @Override
    603     public void setChunkedStreamingMode(int chunklen) {
    604       throw throwRequestModificationException();
    605     }
    606 
    607     @Override
    608     public void setInstanceFollowRedirects(boolean followRedirects) {
    609       throw throwRequestModificationException();
    610     }
    611 
    612     @Override
    613     public boolean getInstanceFollowRedirects() {
    614       // Return the platform default.
    615       return super.getInstanceFollowRedirects();
    616     }
    617 
    618     @Override
    619     public void setRequestMethod(String method) throws ProtocolException {
    620       throw throwRequestModificationException();
    621     }
    622 
    623     @Override
    624     public String getRequestMethod() {
    625       return request.method();
    626     }
    627 
    628     // HTTP Response methods
    629 
    630     @Override
    631     public String getHeaderFieldKey(int position) {
    632       // Deal with index 0 meaning "status line"
    633       if (position < 0) {
    634         throw new IllegalArgumentException("Invalid header index: " + position);
    635       }
    636       if (position == 0) {
    637         return null;
    638       }
    639       return response.headers().name(position - 1);
    640     }
    641 
    642     @Override
    643     public String getHeaderField(int position) {
    644       // Deal with index 0 meaning "status line"
    645       if (position < 0) {
    646         throw new IllegalArgumentException("Invalid header index: " + position);
    647       }
    648       if (position == 0) {
    649         return StatusLine.get(response).toString();
    650       }
    651       return response.headers().value(position - 1);
    652     }
    653 
    654     @Override
    655     public String getHeaderField(String fieldName) {
    656       return fieldName == null
    657           ? StatusLine.get(response).toString()
    658           : response.headers().get(fieldName);
    659     }
    660 
    661     @Override
    662     public Map<String, List<String>> getHeaderFields() {
    663       return OkHeaders.toMultimap(response.headers(), StatusLine.get(response).toString());
    664     }
    665 
    666     @Override
    667     public int getResponseCode() throws IOException {
    668       return response.code();
    669     }
    670 
    671     @Override
    672     public String getResponseMessage() throws IOException {
    673       return response.message();
    674     }
    675 
    676     @Override
    677     public InputStream getErrorStream() {
    678       return null;
    679     }
    680 
    681     // HTTP miscellaneous methods
    682 
    683     @Override
    684     public boolean usingProxy() {
    685       // It's safe to return false here, even if a proxy is in use. The problem is we don't
    686       // necessarily know if we're going to use a proxy by the time we ask the cache for a response.
    687       return false;
    688     }
    689 
    690     // URLConnection methods
    691 
    692     @Override
    693     public void setConnectTimeout(int timeout) {
    694       throw throwRequestModificationException();
    695     }
    696 
    697     @Override
    698     public int getConnectTimeout() {
    699       // Impossible to say.
    700       return 0;
    701     }
    702 
    703     @Override
    704     public void setReadTimeout(int timeout) {
    705       throw throwRequestModificationException();
    706     }
    707 
    708     @Override
    709     public int getReadTimeout() {
    710       // Impossible to say.
    711       return 0;
    712     }
    713 
    714     @Override
    715     public Object getContent() throws IOException {
    716       throw throwResponseBodyAccessException();
    717     }
    718 
    719     @Override
    720     public Object getContent(Class[] classes) throws IOException {
    721       throw throwResponseBodyAccessException();
    722     }
    723 
    724     @Override
    725     public InputStream getInputStream() throws IOException {
    726       throw throwResponseBodyAccessException();
    727     }
    728 
    729     @Override
    730     public OutputStream getOutputStream() throws IOException {
    731       throw throwRequestModificationException();
    732     }
    733 
    734     @Override
    735     public void setDoInput(boolean doInput) {
    736       throw throwRequestModificationException();
    737     }
    738 
    739     @Override
    740     public boolean getDoInput() {
    741       return doInput;
    742     }
    743 
    744     @Override
    745     public void setDoOutput(boolean doOutput) {
    746       throw throwRequestModificationException();
    747     }
    748 
    749     @Override
    750     public boolean getDoOutput() {
    751       return doOutput;
    752     }
    753 
    754     @Override
    755     public void setAllowUserInteraction(boolean allowUserInteraction) {
    756       throw throwRequestModificationException();
    757     }
    758 
    759     @Override
    760     public boolean getAllowUserInteraction() {
    761       return false;
    762     }
    763 
    764     @Override
    765     public void setUseCaches(boolean useCaches) {
    766       throw throwRequestModificationException();
    767     }
    768 
    769     @Override
    770     public boolean getUseCaches() {
    771       return super.getUseCaches();
    772     }
    773 
    774     @Override
    775     public void setIfModifiedSince(long ifModifiedSince) {
    776       throw throwRequestModificationException();
    777     }
    778 
    779     @Override
    780     public long getIfModifiedSince() {
    781       return stringToLong(request.headers().get("If-Modified-Since"));
    782     }
    783 
    784     @Override
    785     public boolean getDefaultUseCaches() {
    786       return super.getDefaultUseCaches();
    787     }
    788 
    789     @Override
    790     public void setDefaultUseCaches(boolean defaultUseCaches) {
    791       super.setDefaultUseCaches(defaultUseCaches);
    792     }
    793   }
    794 
    795   /** An HttpsURLConnection to offer to the cache. */
    796   private static final class CacheHttpsURLConnection extends DelegatingHttpsURLConnection {
    797     private final CacheHttpURLConnection delegate;
    798 
    799     public CacheHttpsURLConnection(CacheHttpURLConnection delegate) {
    800       super(delegate);
    801       this.delegate = delegate;
    802     }
    803 
    804     @Override protected Handshake handshake() {
    805       return delegate.response.handshake();
    806     }
    807 
    808     @Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
    809       throw throwRequestModificationException();
    810     }
    811 
    812     @Override public HostnameVerifier getHostnameVerifier() {
    813       throw throwRequestSslAccessException();
    814     }
    815 
    816     @Override public void setSSLSocketFactory(SSLSocketFactory socketFactory) {
    817       throw throwRequestModificationException();
    818     }
    819 
    820     @Override public SSLSocketFactory getSSLSocketFactory() {
    821       throw throwRequestSslAccessException();
    822     }
    823 
    824     @Override public long getContentLengthLong() {
    825       return delegate.getContentLengthLong();
    826     }
    827 
    828     @Override public void setFixedLengthStreamingMode(long contentLength) {
    829       delegate.setFixedLengthStreamingMode(contentLength);
    830     }
    831 
    832     @Override public long getHeaderFieldLong(String field, long defaultValue) {
    833       return delegate.getHeaderFieldLong(field, defaultValue);
    834     }
    835   }
    836 
    837   private static RuntimeException throwRequestModificationException() {
    838     throw new UnsupportedOperationException("ResponseCache cannot modify the request.");
    839   }
    840 
    841   private static RuntimeException throwRequestHeaderAccessException() {
    842     throw new UnsupportedOperationException("ResponseCache cannot access request headers");
    843   }
    844 
    845   private static RuntimeException throwRequestSslAccessException() {
    846     throw new UnsupportedOperationException("ResponseCache cannot access SSL internals");
    847   }
    848 
    849   private static RuntimeException throwResponseBodyAccessException() {
    850     throw new UnsupportedOperationException("ResponseCache cannot access the response body.");
    851   }
    852 
    853   private static <T> List<T> nullSafeImmutableList(T[] elements) {
    854     return elements == null ? Collections.<T>emptyList() : Util.immutableList(elements);
    855   }
    856 
    857   private static long stringToLong(String s) {
    858     if (s == null) return -1;
    859     try {
    860       return Long.parseLong(s);
    861     } catch (NumberFormatException e) {
    862       return -1;
    863     }
    864   }
    865 }
    866