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