JavaApiConverter.java revision cefd6c9fbb2b15cda911fa662b78cad479e8bba4
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 */
16package com.squareup.okhttp.internal.http;
17
18import com.squareup.okhttp.Handshake;
19import com.squareup.okhttp.Headers;
20import com.squareup.okhttp.MediaType;
21import com.squareup.okhttp.Request;
22import com.squareup.okhttp.Response;
23import com.squareup.okhttp.ResponseSource;
24import com.squareup.okhttp.internal.Util;
25import java.io.IOException;
26import java.io.InputStream;
27import java.io.OutputStream;
28import java.net.CacheResponse;
29import java.net.HttpURLConnection;
30import java.net.ProtocolException;
31import java.net.SecureCacheResponse;
32import java.net.URI;
33import java.net.URLConnection;
34import java.security.Principal;
35import java.security.cert.Certificate;
36import java.util.Collections;
37import java.util.List;
38import java.util.Map;
39import javax.net.ssl.HostnameVerifier;
40import javax.net.ssl.HttpsURLConnection;
41import javax.net.ssl.SSLPeerUnverifiedException;
42import javax.net.ssl.SSLSocketFactory;
43
44/**
45 * Helper methods that convert between Java and OkHttp representations.
46 */
47public final class JavaApiConverter {
48
49  private JavaApiConverter() {
50  }
51
52  /**
53   * Creates an OkHttp {@link Response} using the supplied {@link URI} and {@link URLConnection}
54   * to supply the data. The URLConnection is assumed to already be connected.
55   */
56  public static Response createOkResponse(URI uri, URLConnection urlConnection) throws IOException {
57    HttpURLConnection httpUrlConnection = (HttpURLConnection) urlConnection;
58
59    Response.Builder okResponseBuilder = new Response.Builder();
60
61    // Request: Create one from the URL connection.
62    // A connected HttpURLConnection does not permit access to request headers.
63    Map<String, List<String>> requestHeaders = null;
64    Request okRequest = createOkRequest(uri, httpUrlConnection.getRequestMethod(), requestHeaders);
65    okResponseBuilder.request(okRequest);
66
67    // Status line
68    String statusLine = extractStatusLine(httpUrlConnection);
69    okResponseBuilder.statusLine(statusLine);
70
71    // Response headers
72    Headers okHeaders = extractOkResponseHeaders(httpUrlConnection);
73    okResponseBuilder.headers(okHeaders);
74
75    // Meta data: Defaulted
76    okResponseBuilder.setResponseSource(ResponseSource.NETWORK);
77
78    // Response body
79    Response.Body okBody = createOkBody(okHeaders, urlConnection.getInputStream());
80    okResponseBuilder.body(okBody);
81
82    // Handle SSL handshake information as needed.
83    if (httpUrlConnection instanceof HttpsURLConnection) {
84      HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) httpUrlConnection;
85
86      Certificate[] peerCertificates;
87      try {
88        peerCertificates = httpsUrlConnection.getServerCertificates();
89      } catch (SSLPeerUnverifiedException e) {
90        peerCertificates = null;
91      }
92
93      Certificate[] localCertificates = httpsUrlConnection.getLocalCertificates();
94
95      Handshake handshake = Handshake.get(
96          httpsUrlConnection.getCipherSuite(), nullSafeImmutableList(peerCertificates),
97          nullSafeImmutableList(localCertificates));
98      okResponseBuilder.handshake(handshake);
99    }
100
101    return okResponseBuilder.build();
102  }
103
104  /**
105   * Creates an OkHttp {@link Response} using the supplied {@link Request} and {@link CacheResponse}
106   * to supply the data.
107   */
108  static Response createOkResponse(Request request, CacheResponse javaResponse)
109      throws IOException {
110    Response.Builder okResponseBuilder = new Response.Builder();
111
112    // Request: Use the one provided.
113    okResponseBuilder.request(request);
114
115    // Status line: Java has this as one of the headers.
116    okResponseBuilder.statusLine(extractStatusLine(javaResponse));
117
118    // Response headers
119    Headers okHeaders = extractOkHeaders(javaResponse);
120    okResponseBuilder.headers(okHeaders);
121
122    // Meta data: Defaulted
123    okResponseBuilder.setResponseSource(ResponseSource.CACHE);
124
125    // Response body
126    Response.Body okBody = createOkBody(okHeaders, javaResponse.getBody());
127    okResponseBuilder.body(okBody);
128
129    // Handle SSL handshake information as needed.
130    if (javaResponse instanceof SecureCacheResponse) {
131      SecureCacheResponse javaSecureCacheResponse = (SecureCacheResponse) javaResponse;
132
133      // Handshake doesn't support null lists.
134      List<Certificate> peerCertificates;
135      try {
136        peerCertificates = javaSecureCacheResponse.getServerCertificateChain();
137      } catch (SSLPeerUnverifiedException e) {
138        peerCertificates = Collections.emptyList();
139      }
140      List<Certificate> localCertificates = javaSecureCacheResponse.getLocalCertificateChain();
141      if (localCertificates == null) {
142        localCertificates = Collections.emptyList();
143      }
144      Handshake handshake = Handshake.get(
145          javaSecureCacheResponse.getCipherSuite(), peerCertificates, localCertificates);
146      okResponseBuilder.handshake(handshake);
147    }
148
149    return okResponseBuilder.build();
150  }
151
152  /**
153   * Creates an OkHttp {@link Request} from the supplied information.
154   *
155   * <p>This method allows a {@code null} value for {@code requestHeaders} for situations
156   * where a connection is already connected and access to the headers has been lost.
157   * See {@link java.net.HttpURLConnection#getRequestProperties()} for details.
158   */
159  public static Request createOkRequest(
160      URI uri, String requestMethod, Map<String, List<String>> requestHeaders) {
161
162    Request.Builder builder = new Request.Builder()
163        .url(uri.toString())
164        .method(requestMethod, null);
165
166    if (requestHeaders != null) {
167      Headers headers = extractOkHeaders(requestHeaders);
168      builder.headers(headers);
169    }
170    return builder.build();
171  }
172
173  /**
174   * Creates a {@link java.net.CacheResponse} of the correct (sub)type using information
175   * gathered from the supplied {@link Response}.
176   */
177  public static CacheResponse createJavaCacheResponse(final Response response) {
178    final Headers headers = response.headers();
179    final Response.Body body = response.body();
180    if (response.request().isHttps()) {
181      final Handshake handshake = response.handshake();
182      return new SecureCacheResponse() {
183        @Override
184        public String getCipherSuite() {
185          return handshake != null ? handshake.cipherSuite() : null;
186        }
187
188        @Override
189        public List<Certificate> getLocalCertificateChain() {
190          if (handshake == null) return null;
191          // Java requires null, not an empty list here.
192          List<Certificate> certificates = handshake.localCertificates();
193          return certificates.size() > 0 ? certificates : null;
194        }
195
196        @Override
197        public List<Certificate> getServerCertificateChain() throws SSLPeerUnverifiedException {
198          if (handshake == null) return null;
199          // Java requires null, not an empty list here.
200          List<Certificate> certificates = handshake.peerCertificates();
201          return certificates.size() > 0 ? certificates : null;
202        }
203
204        @Override
205        public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
206          if (handshake == null) return null;
207          return handshake.peerPrincipal();
208        }
209
210        @Override
211        public Principal getLocalPrincipal() {
212          if (handshake == null) return null;
213          return handshake.localPrincipal();
214        }
215
216        @Override
217        public Map<String, List<String>> getHeaders() throws IOException {
218          // Java requires that the entry with a null key be the status line.
219          return OkHeaders.toMultimap(headers, response.statusLine());
220        }
221
222        @Override
223        public InputStream getBody() throws IOException {
224          if (body == null) return null;
225          return body.byteStream();
226        }
227      };
228    } else {
229      return new CacheResponse() {
230        @Override
231        public Map<String, List<String>> getHeaders() throws IOException {
232          // Java requires that the entry with a null key be the status line.
233          return OkHeaders.toMultimap(headers, response.statusLine());
234        }
235
236        @Override
237        public InputStream getBody() throws IOException {
238          if (body == null) return null;
239          return body.byteStream();
240        }
241      };
242    }
243  }
244
245  /**
246   * Creates an {@link java.net.HttpURLConnection} of the correct subclass from the supplied OkHttp
247   * {@link Response}.
248   */
249  static HttpURLConnection createJavaUrlConnection(Response okResponse) {
250    Request request = okResponse.request();
251    // Create an object of the correct class in case the ResponseCache uses instanceof.
252    if (request.isHttps()) {
253      return new CacheHttpsURLConnection(new CacheHttpURLConnection(okResponse));
254    } else {
255      return new CacheHttpURLConnection(okResponse);
256    }
257  }
258
259  /**
260   * Extracts an immutable request header map from the supplied {@link com.squareup.okhttp.Headers}.
261   */
262  static Map<String, List<String>> extractJavaHeaders(Request request) {
263    return OkHeaders.toMultimap(request.headers(), null);
264  }
265
266  /**
267   * Extracts OkHttp headers from the supplied {@link java.net.CacheResponse}. Only real headers are
268   * extracted. See {@link #extractStatusLine(java.net.CacheResponse)}.
269   */
270  private static Headers extractOkHeaders(CacheResponse javaResponse) throws IOException {
271    Map<String, List<String>> javaResponseHeaders = javaResponse.getHeaders();
272    return extractOkHeaders(javaResponseHeaders);
273  }
274
275  /**
276   * Extracts OkHttp headers from the supplied {@link java.net.HttpURLConnection}. Only real headers
277   * are extracted. See {@link #extractStatusLine(java.net.HttpURLConnection)}.
278   */
279  private static Headers extractOkResponseHeaders(HttpURLConnection httpUrlConnection) {
280    Map<String, List<String>> javaResponseHeaders = httpUrlConnection.getHeaderFields();
281    return extractOkHeaders(javaResponseHeaders);
282  }
283
284  /**
285   * Extracts OkHttp headers from the supplied {@link Map}. Only real headers are
286   * extracted. Any entry (one with a {@code null} key) is discarded.
287   */
288  // @VisibleForTesting
289  static Headers extractOkHeaders(Map<String, List<String>> javaHeaders) {
290    Headers.Builder okHeadersBuilder = new Headers.Builder();
291    for (Map.Entry<String, List<String>> javaHeader : javaHeaders.entrySet()) {
292      String name = javaHeader.getKey();
293      if (name == null) {
294        // The Java API uses the null key to store the status line in responses.
295        // Earlier versions of OkHttp would use the null key to store the "request line" in
296        // requests. e.g. "GET / HTTP 1.1". Although this is no longer the case it must be
297        // explicitly ignored because Headers.Builder does not support null keys.
298        continue;
299      }
300      for (String value : javaHeader.getValue()) {
301        okHeadersBuilder.add(name, value);
302      }
303    }
304    return okHeadersBuilder.build();
305  }
306
307  /**
308   * Extracts the status line from the supplied Java API {@link java.net.HttpURLConnection}.
309   * As per the spec, the status line is held as the header with the null key. Returns {@code null}
310   * if there is no status line.
311   */
312  private static String extractStatusLine(HttpURLConnection httpUrlConnection) {
313    // Java specifies that this will be be response header with a null key.
314    return httpUrlConnection.getHeaderField(null);
315  }
316
317  /**
318   * Extracts the status line from the supplied Java API {@link java.net.CacheResponse}.
319   * As per the spec, the status line is held as the header with the null key. Returns {@code null}
320   * if there is no status line.
321   */
322  private static String extractStatusLine(CacheResponse javaResponse) throws IOException {
323    Map<String, List<String>> javaResponseHeaders = javaResponse.getHeaders();
324    return extractStatusLine(javaResponseHeaders);
325  }
326
327  // VisibleForTesting
328  static String extractStatusLine(Map<String, List<String>> javaResponseHeaders) {
329    List<String> values = javaResponseHeaders.get(null);
330    if (values == null || values.size() == 0) {
331      return null;
332    }
333    return values.get(0);
334  }
335
336  /**
337   * Creates an OkHttp Response.Body containing the supplied information.
338   */
339  private static Response.Body createOkBody(final Headers okHeaders, final InputStream body) {
340    return new Response.Body() {
341
342      @Override
343      public boolean ready() throws IOException {
344        return true;
345      }
346
347      @Override
348      public MediaType contentType() {
349        String contentTypeHeader = okHeaders.get("Content-Type");
350        return contentTypeHeader == null ? null : MediaType.parse(contentTypeHeader);
351      }
352
353      @Override
354      public long contentLength() {
355        return OkHeaders.contentLength(okHeaders);
356      }
357
358      @Override
359      public InputStream byteStream() {
360        return body;
361      }
362    };
363  }
364
365  /**
366   * An {@link java.net.HttpURLConnection} that represents an HTTP request at the point where
367   * the request has been made, and the response headers have been received, but the body content,
368   * if present, has not been read yet. This intended to provide enough information for
369   * {@link java.net.ResponseCache} subclasses and no more.
370   *
371   * <p>Much of the method implementations are overrides to delegate to the OkHttp request and
372   * response, or to deny access to information as a real HttpURLConnection would after connection.
373   */
374  private static final class CacheHttpURLConnection extends HttpURLConnection {
375
376    private final Request request;
377    private final Response response;
378
379    public CacheHttpURLConnection(Response response) {
380      super(response.request().url());
381      this.request = response.request();
382      this.response = response;
383
384      // Configure URLConnection inherited fields.
385      this.connected = true;
386      this.doOutput = response.body() == null;
387
388      // Configure HttpUrlConnection inherited fields.
389      this.method = request.method();
390    }
391
392    // HTTP connection lifecycle methods
393
394    @Override
395    public void connect() throws IOException {
396      throw throwRequestModificationException();
397    }
398
399    @Override
400    public void disconnect() {
401      throw throwRequestModificationException();
402    }
403
404    // HTTP Request methods
405
406    @Override
407    public void setRequestProperty(String key, String value) {
408      throw throwRequestModificationException();
409    }
410
411    @Override
412    public void addRequestProperty(String key, String value) {
413      throw throwRequestModificationException();
414    }
415
416    @Override
417    public String getRequestProperty(String key) {
418      return request.header(key);
419    }
420
421    @Override
422    public Map<String, List<String>> getRequestProperties() {
423      // This is to preserve RI and compatibility with OkHttp's HttpURLConnectionImpl. There seems
424      // no good reason why this should fail while getRequestProperty() is ok.
425      throw throwRequestHeaderAccessException();
426    }
427
428    @Override
429    public void setFixedLengthStreamingMode(int contentLength) {
430      throw throwRequestModificationException();
431    }
432
433    @Override
434    public void setFixedLengthStreamingMode(long contentLength) {
435      throw throwRequestModificationException();
436    }
437
438    @Override
439    public void setChunkedStreamingMode(int chunklen) {
440      throw throwRequestModificationException();
441    }
442
443    @Override
444    public void setInstanceFollowRedirects(boolean followRedirects) {
445      throw throwRequestModificationException();
446    }
447
448    @Override
449    public boolean getInstanceFollowRedirects() {
450      // Return the platform default.
451      return super.getInstanceFollowRedirects();
452    }
453
454    @Override
455    public void setRequestMethod(String method) throws ProtocolException {
456      throw throwRequestModificationException();
457    }
458
459    @Override
460    public String getRequestMethod() {
461      return request.method();
462    }
463
464    // HTTP Response methods
465
466    @Override
467    public String getHeaderFieldKey(int position) {
468      // Deal with index 0 meaning "status line"
469      if (position < 0) {
470        throw new IllegalArgumentException("Invalid header index: " + position);
471      }
472      if (position == 0) {
473        return null;
474      }
475      return response.headers().name(position - 1);
476    }
477
478    @Override
479    public String getHeaderField(int position) {
480      // Deal with index 0 meaning "status line"
481      if (position < 0) {
482        throw new IllegalArgumentException("Invalid header index: " + position);
483      }
484      if (position == 0) {
485        return response.statusLine();
486      }
487      return response.headers().value(position - 1);
488    }
489
490    @Override
491    public String getHeaderField(String fieldName) {
492      return fieldName == null ? response.statusLine() : response.headers().get(fieldName);
493    }
494
495    @Override
496    public Map<String, List<String>> getHeaderFields() {
497      return OkHeaders.toMultimap(response.headers(), response.statusLine());
498    }
499
500    @Override
501    public int getResponseCode() throws IOException {
502      return response.code();
503    }
504
505    @Override
506    public String getResponseMessage() throws IOException {
507      return response.statusMessage();
508    }
509
510    @Override
511    public InputStream getErrorStream() {
512      return null;
513    }
514
515    // HTTP miscellaneous methods
516
517    @Override
518    public boolean usingProxy() {
519      // It's safe to return false here, even if a proxy is in use. The problem is we don't
520      // necessarily know if we're going to use a proxy by the time we ask the cache for a response.
521      return false;
522    }
523
524    // URLConnection methods
525
526    @Override
527    public void setConnectTimeout(int timeout) {
528      throw throwRequestModificationException();
529    }
530
531    @Override
532    public int getConnectTimeout() {
533      // Impossible to say.
534      return 0;
535    }
536
537    @Override
538    public void setReadTimeout(int timeout) {
539      throw throwRequestModificationException();
540    }
541
542    @Override
543    public int getReadTimeout() {
544      // Impossible to say.
545      return 0;
546    }
547
548    @Override
549    public Object getContent() throws IOException {
550      throw throwResponseBodyAccessException();
551    }
552
553    @Override
554    public Object getContent(Class[] classes) throws IOException {
555      throw throwResponseBodyAccessException();
556    }
557
558    @Override
559    public InputStream getInputStream() throws IOException {
560      throw throwResponseBodyAccessException();
561    }
562
563    @Override
564    public OutputStream getOutputStream() throws IOException {
565      throw throwRequestModificationException();
566    }
567
568    @Override
569    public void setDoInput(boolean doInput) {
570      throw throwRequestModificationException();
571    }
572
573    @Override
574    public boolean getDoInput() {
575      return true;
576    }
577
578    @Override
579    public void setDoOutput(boolean doOutput) {
580      throw throwRequestModificationException();
581    }
582
583    @Override
584    public boolean getDoOutput() {
585      return request.body() != null;
586    }
587
588    @Override
589    public void setAllowUserInteraction(boolean allowUserInteraction) {
590      throw throwRequestModificationException();
591    }
592
593    @Override
594    public boolean getAllowUserInteraction() {
595      return false;
596    }
597
598    @Override
599    public void setUseCaches(boolean useCaches) {
600      throw throwRequestModificationException();
601    }
602
603    @Override
604    public boolean getUseCaches() {
605      return super.getUseCaches();
606    }
607
608    @Override
609    public void setIfModifiedSince(long ifModifiedSince) {
610      throw throwRequestModificationException();
611    }
612
613    @Override
614    public long getIfModifiedSince() {
615      return 0;
616    }
617
618    @Override
619    public boolean getDefaultUseCaches() {
620      return super.getDefaultUseCaches();
621    }
622
623    @Override
624    public void setDefaultUseCaches(boolean defaultUseCaches) {
625      super.setDefaultUseCaches(defaultUseCaches);
626    }
627  }
628
629  /** An HttpsURLConnection to offer to the cache. */
630  private static final class CacheHttpsURLConnection extends DelegatingHttpsURLConnection {
631    private final CacheHttpURLConnection delegate;
632
633    public CacheHttpsURLConnection(CacheHttpURLConnection delegate) {
634      super(delegate);
635      this.delegate = delegate;
636    }
637
638    @Override protected Handshake handshake() {
639      return delegate.response.handshake();
640    }
641
642    @Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
643      throw throwRequestModificationException();
644    }
645
646    @Override public HostnameVerifier getHostnameVerifier() {
647      throw throwRequestSslAccessException();
648    }
649
650    @Override public void setSSLSocketFactory(SSLSocketFactory socketFactory) {
651      throw throwRequestModificationException();
652    }
653
654    @Override public SSLSocketFactory getSSLSocketFactory() {
655      throw throwRequestSslAccessException();
656    }
657
658    @Override public void setFixedLengthStreamingMode(long contentLength) {
659      delegate.setFixedLengthStreamingMode(contentLength);
660    }
661  }
662
663  private static RuntimeException throwRequestModificationException() {
664    throw new UnsupportedOperationException("ResponseCache cannot modify the request.");
665  }
666
667  private static RuntimeException throwRequestHeaderAccessException() {
668    throw new UnsupportedOperationException("ResponseCache cannot access request headers");
669  }
670
671  private static RuntimeException throwRequestSslAccessException() {
672    throw new UnsupportedOperationException("ResponseCache cannot access SSL internals");
673  }
674
675  private static RuntimeException throwResponseBodyAccessException() {
676    throw new UnsupportedOperationException("ResponseCache cannot access the response body.");
677  }
678
679  private static <T> List<T> nullSafeImmutableList(T[] elements) {
680    return elements == null ? Collections.<T>emptyList() : Util.immutableList(elements);
681  }
682
683}
684