HttpURLConnectionImpl.java revision a82f42bbeedd0b07f3892f3b0efaa8122dc8f264
1/* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.squareup.okhttp.internal.http; 19 20import com.squareup.okhttp.Connection; 21import com.squareup.okhttp.OkHttpClient; 22import com.squareup.okhttp.internal.AbstractOutputStream; 23import com.squareup.okhttp.internal.FaultRecoveringOutputStream; 24import com.squareup.okhttp.internal.Platform; 25import com.squareup.okhttp.internal.Util; 26import java.io.FileNotFoundException; 27import java.io.IOException; 28import java.io.InputStream; 29import java.io.OutputStream; 30import java.net.HttpRetryException; 31import java.net.HttpURLConnection; 32import java.net.InetSocketAddress; 33import java.net.ProtocolException; 34import java.net.Proxy; 35import java.net.SocketPermission; 36import java.net.URL; 37import java.security.Permission; 38import java.security.cert.CertificateException; 39import java.util.ArrayList; 40import java.util.List; 41import java.util.Map; 42import java.util.concurrent.TimeUnit; 43import javax.net.ssl.SSLHandshakeException; 44 45import static com.squareup.okhttp.internal.Util.getEffectivePort; 46 47/** 48 * This implementation uses HttpEngine to send requests and receive responses. 49 * This class may use multiple HttpEngines to follow redirects, authentication 50 * retries, etc. to retrieve the final response body. 51 * 52 * <h3>What does 'connected' mean?</h3> 53 * This class inherits a {@code connected} field from the superclass. That field 54 * is <strong>not</strong> used to indicate not whether this URLConnection is 55 * currently connected. Instead, it indicates whether a connection has ever been 56 * attempted. Once a connection has been attempted, certain properties (request 57 * header fields, request method, etc.) are immutable. Test the {@code 58 * connection} field on this class for null/non-null to determine of an instance 59 * is currently connected to a server. 60 */ 61public class HttpURLConnectionImpl extends HttpURLConnection implements Policy { 62 63 /** Numeric status code, 307: Temporary Redirect. */ 64 static final int HTTP_TEMP_REDIRECT = 307; 65 66 /** 67 * How many redirects should we follow? Chrome follows 21; Firefox, curl, 68 * and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5. 69 */ 70 private static final int MAX_REDIRECTS = 20; 71 72 /** 73 * The minimum number of request body bytes to transmit before we're willing 74 * to let a routine {@link IOException} bubble up to the user. This is used to 75 * size a buffer for data that will be replayed upon error. 76 */ 77 private static final int MAX_REPLAY_BUFFER_LENGTH = 8192; 78 79 final OkHttpClient client; 80 81 private final RawHeaders rawRequestHeaders = new RawHeaders(); 82 /** Like the superclass field of the same name, but a long and available on all platforms. */ 83 private long fixedContentLength = -1; 84 private int redirectionCount; 85 private FaultRecoveringOutputStream faultRecoveringRequestBody; 86 protected IOException httpEngineFailure; 87 protected HttpEngine httpEngine; 88 89 public HttpURLConnectionImpl(URL url, OkHttpClient client) { 90 super(url); 91 this.client = client; 92 } 93 94 @Override public final void connect() throws IOException { 95 initHttpEngine(); 96 boolean success; 97 do { 98 success = execute(false); 99 } while (!success); 100 } 101 102 @Override public final void disconnect() { 103 // Calling disconnect() before a connection exists should have no effect. 104 if (httpEngine != null) { 105 // We close the response body here instead of in 106 // HttpEngine.release because that is called when input 107 // has been completely read from the underlying socket. 108 // However the response body can be a GZIPInputStream that 109 // still has unread data. 110 if (httpEngine.hasResponse()) { 111 Util.closeQuietly(httpEngine.getResponseBody()); 112 } 113 httpEngine.release(true); 114 } 115 } 116 117 /** 118 * Returns an input stream from the server in the case of error such as the 119 * requested file (txt, htm, html) is not found on the remote server. 120 */ 121 @Override public final InputStream getErrorStream() { 122 try { 123 HttpEngine response = getResponse(); 124 if (response.hasResponseBody() && response.getResponseCode() >= HTTP_BAD_REQUEST) { 125 return response.getResponseBody(); 126 } 127 return null; 128 } catch (IOException e) { 129 return null; 130 } 131 } 132 133 /** 134 * Returns the value of the field at {@code position}. Returns null if there 135 * are fewer than {@code position} headers. 136 */ 137 @Override public final String getHeaderField(int position) { 138 try { 139 return getResponse().getResponseHeaders().getHeaders().getValue(position); 140 } catch (IOException e) { 141 return null; 142 } 143 } 144 145 /** 146 * Returns the value of the field corresponding to the {@code fieldName}, or 147 * null if there is no such field. If the field has multiple values, the 148 * last value is returned. 149 */ 150 @Override public final String getHeaderField(String fieldName) { 151 try { 152 RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders(); 153 return fieldName == null ? rawHeaders.getStatusLine() : rawHeaders.get(fieldName); 154 } catch (IOException e) { 155 return null; 156 } 157 } 158 159 @Override public final String getHeaderFieldKey(int position) { 160 try { 161 return getResponse().getResponseHeaders().getHeaders().getFieldName(position); 162 } catch (IOException e) { 163 return null; 164 } 165 } 166 167 @Override public final Map<String, List<String>> getHeaderFields() { 168 try { 169 return getResponse().getResponseHeaders().getHeaders().toMultimap(true); 170 } catch (IOException e) { 171 return null; 172 } 173 } 174 175 @Override public final Map<String, List<String>> getRequestProperties() { 176 if (connected) { 177 throw new IllegalStateException( 178 "Cannot access request header fields after connection is set"); 179 } 180 return rawRequestHeaders.toMultimap(false); 181 } 182 183 @Override public final InputStream getInputStream() throws IOException { 184 if (!doInput) { 185 throw new ProtocolException("This protocol does not support input"); 186 } 187 188 HttpEngine response = getResponse(); 189 190 // if the requested file does not exist, throw an exception formerly the 191 // Error page from the server was returned if the requested file was 192 // text/html this has changed to return FileNotFoundException for all 193 // file types 194 if (getResponseCode() >= HTTP_BAD_REQUEST) { 195 throw new FileNotFoundException(url.toString()); 196 } 197 198 InputStream result = response.getResponseBody(); 199 if (result == null) { 200 throw new ProtocolException("No response body exists; responseCode=" + getResponseCode()); 201 } 202 return result; 203 } 204 205 @Override public final OutputStream getOutputStream() throws IOException { 206 connect(); 207 208 OutputStream out = httpEngine.getRequestBody(); 209 if (out == null) { 210 throw new ProtocolException("method does not support a request body: " + method); 211 } else if (httpEngine.hasResponse()) { 212 throw new ProtocolException("cannot write request body after response has been read"); 213 } 214 215 if (faultRecoveringRequestBody == null) { 216 faultRecoveringRequestBody = new FaultRecoveringOutputStream(MAX_REPLAY_BUFFER_LENGTH, out) { 217 @Override protected OutputStream replacementStream(IOException e) throws IOException { 218 if (httpEngine.getRequestBody() instanceof AbstractOutputStream 219 && ((AbstractOutputStream) httpEngine.getRequestBody()).isClosed()) { 220 return null; // Don't recover once the underlying stream has been closed. 221 } 222 if (handleFailure(e)) { 223 return httpEngine.getRequestBody(); 224 } 225 return null; // This is a permanent failure. 226 } 227 }; 228 } 229 230 return faultRecoveringRequestBody; 231 } 232 233 @Override public final Permission getPermission() throws IOException { 234 String hostName = getURL().getHost(); 235 int hostPort = Util.getEffectivePort(getURL()); 236 if (usingProxy()) { 237 InetSocketAddress proxyAddress = (InetSocketAddress) client.getProxy().address(); 238 hostName = proxyAddress.getHostName(); 239 hostPort = proxyAddress.getPort(); 240 } 241 return new SocketPermission(hostName + ":" + hostPort, "connect, resolve"); 242 } 243 244 @Override public final String getRequestProperty(String field) { 245 if (field == null) { 246 return null; 247 } 248 return rawRequestHeaders.get(field); 249 } 250 251 @Override public void setConnectTimeout(int timeoutMillis) { 252 client.setConnectTimeout(timeoutMillis, TimeUnit.MILLISECONDS); 253 } 254 255 @Override public int getConnectTimeout() { 256 return client.getConnectTimeout(); 257 } 258 259 @Override public void setReadTimeout(int timeoutMillis) { 260 client.setReadTimeout(timeoutMillis, TimeUnit.MILLISECONDS); 261 } 262 263 @Override public int getReadTimeout() { 264 return client.getReadTimeout(); 265 } 266 267 private void initHttpEngine() throws IOException { 268 if (httpEngineFailure != null) { 269 throw httpEngineFailure; 270 } else if (httpEngine != null) { 271 return; 272 } 273 274 connected = true; 275 try { 276 if (doOutput) { 277 if (method.equals("GET")) { 278 // they are requesting a stream to write to. This implies a POST method 279 method = "POST"; 280 } else if (!method.equals("POST") && !method.equals("PUT")) { 281 // If the request method is neither POST nor PUT, then you're not writing 282 throw new ProtocolException(method + " does not support writing"); 283 } 284 } 285 httpEngine = newHttpEngine(method, rawRequestHeaders, null, null); 286 } catch (IOException e) { 287 httpEngineFailure = e; 288 throw e; 289 } 290 } 291 292 @Override public HttpURLConnection getHttpConnectionToCache() { 293 return this; 294 } 295 296 private HttpEngine newHttpEngine(String method, RawHeaders requestHeaders, 297 Connection connection, RetryableOutputStream requestBody) throws IOException { 298 if (url.getProtocol().equals("http")) { 299 return new HttpEngine(client, this, method, requestHeaders, connection, requestBody); 300 } else if (url.getProtocol().equals("https")) { 301 return new HttpsEngine(client, this, method, requestHeaders, connection, requestBody); 302 } else { 303 throw new AssertionError(); 304 } 305 } 306 307 /** 308 * Aggressively tries to get the final HTTP response, potentially making 309 * many HTTP requests in the process in order to cope with redirects and 310 * authentication. 311 */ 312 private HttpEngine getResponse() throws IOException { 313 initHttpEngine(); 314 315 if (httpEngine.hasResponse()) { 316 return httpEngine; 317 } 318 319 while (true) { 320 if (!execute(true)) { 321 continue; 322 } 323 324 Retry retry = processResponseHeaders(); 325 if (retry == Retry.NONE) { 326 httpEngine.automaticallyReleaseConnectionToPool(); 327 return httpEngine; 328 } 329 330 // The first request was insufficient. Prepare for another... 331 String retryMethod = method; 332 OutputStream requestBody = httpEngine.getRequestBody(); 333 334 // Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM 335 // redirect should keep the same method, Chrome, Firefox and the 336 // RI all issue GETs when following any redirect. 337 int responseCode = getResponseCode(); 338 if (responseCode == HTTP_MULT_CHOICE 339 || responseCode == HTTP_MOVED_PERM 340 || responseCode == HTTP_MOVED_TEMP 341 || responseCode == HTTP_SEE_OTHER) { 342 retryMethod = "GET"; 343 requestBody = null; 344 } 345 346 if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) { 347 throw new HttpRetryException("Cannot retry streamed HTTP body", 348 httpEngine.getResponseCode()); 349 } 350 351 if (retry == Retry.DIFFERENT_CONNECTION) { 352 httpEngine.automaticallyReleaseConnectionToPool(); 353 } 354 355 httpEngine.release(false); 356 357 httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(), 358 (RetryableOutputStream) requestBody); 359 } 360 } 361 362 /** 363 * Sends a request and optionally reads a response. Returns true if the 364 * request was successfully executed, and false if the request can be 365 * retried. Throws an exception if the request failed permanently. 366 */ 367 private boolean execute(boolean readResponse) throws IOException { 368 try { 369 httpEngine.sendRequest(); 370 if (readResponse) { 371 httpEngine.readResponse(); 372 } 373 return true; 374 } catch (IOException e) { 375 if (handleFailure(e)) { 376 return false; 377 } else { 378 throw e; 379 } 380 } 381 } 382 383 /** 384 * Report and attempt to recover from {@code e}. Returns true if the HTTP 385 * engine was replaced and the request should be retried. Otherwise the 386 * failure is permanent. 387 */ 388 private boolean handleFailure(IOException e) throws IOException { 389 RouteSelector routeSelector = httpEngine.routeSelector; 390 if (routeSelector != null && httpEngine.connection != null) { 391 routeSelector.connectFailed(httpEngine.connection, e); 392 } 393 394 OutputStream requestBody = httpEngine.getRequestBody(); 395 boolean canRetryRequestBody = requestBody == null 396 || requestBody instanceof RetryableOutputStream 397 || (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable()); 398 if (routeSelector == null && httpEngine.connection == null // No connection. 399 || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt. 400 || !isRecoverable(e) 401 || !canRetryRequestBody) { 402 httpEngineFailure = e; 403 return false; 404 } 405 406 httpEngine.release(true); 407 RetryableOutputStream retryableOutputStream = requestBody instanceof RetryableOutputStream 408 ? (RetryableOutputStream) requestBody 409 : null; 410 httpEngine = newHttpEngine(method, rawRequestHeaders, null, retryableOutputStream); 411 httpEngine.routeSelector = routeSelector; // Keep the same routeSelector. 412 if (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable()) { 413 httpEngine.sendRequest(); 414 faultRecoveringRequestBody.replaceStream(httpEngine.getRequestBody()); 415 } 416 return true; 417 } 418 419 private boolean isRecoverable(IOException e) { 420 // If the problem was a CertificateException from the X509TrustManager, 421 // do not retry, we didn't have an abrupt server initiated exception. 422 boolean sslFailure = 423 e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException; 424 boolean protocolFailure = e instanceof ProtocolException; 425 return !sslFailure && !protocolFailure; 426 } 427 428 public HttpEngine getHttpEngine() { 429 return httpEngine; 430 } 431 432 enum Retry { 433 NONE, 434 SAME_CONNECTION, 435 DIFFERENT_CONNECTION 436 } 437 438 /** 439 * Returns the retry action to take for the current response headers. The 440 * headers, proxy and target URL or this connection may be adjusted to 441 * prepare for a follow up request. 442 */ 443 private Retry processResponseHeaders() throws IOException { 444 Proxy selectedProxy = httpEngine.connection != null 445 ? httpEngine.connection.getRoute().getProxy() 446 : client.getProxy(); 447 final int responseCode = getResponseCode(); 448 switch (responseCode) { 449 case HTTP_PROXY_AUTH: 450 if (selectedProxy.type() != Proxy.Type.HTTP) { 451 throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy"); 452 } 453 // fall-through 454 case HTTP_UNAUTHORIZED: 455 boolean credentialsFound = HttpAuthenticator.processAuthHeader(client.getAuthenticator(), 456 getResponseCode(), httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, 457 selectedProxy, url); 458 return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE; 459 460 case HTTP_MULT_CHOICE: 461 case HTTP_MOVED_PERM: 462 case HTTP_MOVED_TEMP: 463 case HTTP_SEE_OTHER: 464 case HTTP_TEMP_REDIRECT: 465 if (!getInstanceFollowRedirects()) { 466 return Retry.NONE; 467 } 468 if (++redirectionCount > MAX_REDIRECTS) { 469 throw new ProtocolException("Too many redirects: " + redirectionCount); 470 } 471 if (responseCode == HTTP_TEMP_REDIRECT && !method.equals("GET") && !method.equals("HEAD")) { 472 // "If the 307 status code is received in response to a request other than GET or HEAD, 473 // the user agent MUST NOT automatically redirect the request" 474 return Retry.NONE; 475 } 476 String location = getHeaderField("Location"); 477 if (location == null) { 478 return Retry.NONE; 479 } 480 URL previousUrl = url; 481 url = new URL(previousUrl, location); 482 if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) { 483 return Retry.NONE; // Don't follow redirects to unsupported protocols. 484 } 485 boolean sameProtocol = previousUrl.getProtocol().equals(url.getProtocol()); 486 if (!sameProtocol && !client.getFollowProtocolRedirects()) { 487 return Retry.NONE; // This client doesn't follow redirects across protocols. 488 } 489 boolean sameHost = previousUrl.getHost().equals(url.getHost()); 490 boolean samePort = getEffectivePort(previousUrl) == getEffectivePort(url); 491 if (sameHost && samePort && sameProtocol) { 492 return Retry.SAME_CONNECTION; 493 } else { 494 return Retry.DIFFERENT_CONNECTION; 495 } 496 497 default: 498 return Retry.NONE; 499 } 500 } 501 502 /** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */ 503 @Override public final long getFixedContentLength() { 504 return fixedContentLength; 505 } 506 507 @Override public final int getChunkLength() { 508 return chunkLength; 509 } 510 511 @Override public final boolean usingProxy() { 512 Proxy proxy = client.getProxy(); 513 return proxy != null && proxy.type() != Proxy.Type.DIRECT; 514 } 515 516 @Override public String getResponseMessage() throws IOException { 517 return getResponse().getResponseHeaders().getHeaders().getResponseMessage(); 518 } 519 520 @Override public final int getResponseCode() throws IOException { 521 return getResponse().getResponseCode(); 522 } 523 524 @Override public final void setRequestProperty(String field, String newValue) { 525 if (connected) { 526 throw new IllegalStateException("Cannot set request property after connection is made"); 527 } 528 if (field == null) { 529 throw new NullPointerException("field == null"); 530 } 531 if (newValue == null) { 532 // Silently ignore null header values for backwards compatibility with older 533 // android versions as well as with other URLConnection implementations. 534 // 535 // Some implementations send a malformed HTTP header when faced with 536 // such requests, we respect the spec and ignore the header. 537 Platform.get().logW("Ignoring header " + field + " because its value was null."); 538 return; 539 } 540 541 if ("X-Android-Transports".equals(field)) { 542 setTransports(newValue, false /* append */); 543 } else { 544 rawRequestHeaders.set(field, newValue); 545 } 546 } 547 548 @Override public final void addRequestProperty(String field, String value) { 549 if (connected) { 550 throw new IllegalStateException("Cannot add request property after connection is made"); 551 } 552 if (field == null) { 553 throw new NullPointerException("field == null"); 554 } 555 if (value == null) { 556 // Silently ignore null header values for backwards compatibility with older 557 // android versions as well as with other URLConnection implementations. 558 // 559 // Some implementations send a malformed HTTP header when faced with 560 // such requests, we respect the spec and ignore the header. 561 Platform.get().logW("Ignoring header " + field + " because its value was null."); 562 return; 563 } 564 565 if ("X-Android-Transports".equals(field)) { 566 setTransports(value, true /* append */); 567 } else { 568 rawRequestHeaders.add(field, value); 569 } 570 } 571 572 /* 573 * Splits and validates a comma-separated string of transports. 574 * When append == false, we require that the transport list contains "http/1.1". 575 */ 576 private void setTransports(String transportsString, boolean append) { 577 List<String> transportsList = new ArrayList<String>(); 578 if (append) { 579 transportsList.addAll(client.getTransports()); 580 } 581 for (String transport : transportsString.split(",", -1)) { 582 transportsList.add(transport); 583 } 584 client.setTransports(transportsList); 585 } 586 587 @Override public void setFixedLengthStreamingMode(int contentLength) { 588 setFixedLengthStreamingMode((long) contentLength); 589 } 590 591 // @Override Don't override: this overload method doesn't exist prior to Java 1.7. 592 public void setFixedLengthStreamingMode(long contentLength) { 593 if (super.connected) throw new IllegalStateException("Already connected"); 594 if (chunkLength > 0) throw new IllegalStateException("Already in chunked mode"); 595 if (contentLength < 0) throw new IllegalArgumentException("contentLength < 0"); 596 this.fixedContentLength = contentLength; 597 super.fixedContentLength = (int) Math.min(contentLength, Integer.MAX_VALUE); 598 } 599} 600