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 18 package com.squareup.okhttp.internal.huc; 19 20 import com.squareup.okhttp.Connection; 21 import com.squareup.okhttp.Handshake; 22 import com.squareup.okhttp.Headers; 23 import com.squareup.okhttp.HttpUrl; 24 import com.squareup.okhttp.OkHttpClient; 25 import com.squareup.okhttp.Protocol; 26 import com.squareup.okhttp.Request; 27 import com.squareup.okhttp.RequestBody; 28 import com.squareup.okhttp.Response; 29 import com.squareup.okhttp.Route; 30 import com.squareup.okhttp.internal.Internal; 31 import com.squareup.okhttp.internal.Platform; 32 import com.squareup.okhttp.internal.URLFilter; 33 import com.squareup.okhttp.internal.Util; 34 import com.squareup.okhttp.internal.Version; 35 import com.squareup.okhttp.internal.http.HttpDate; 36 import com.squareup.okhttp.internal.http.HttpEngine; 37 import com.squareup.okhttp.internal.http.HttpMethod; 38 import com.squareup.okhttp.internal.http.OkHeaders; 39 import com.squareup.okhttp.internal.http.RequestException; 40 import com.squareup.okhttp.internal.http.RetryableSink; 41 import com.squareup.okhttp.internal.http.RouteException; 42 import com.squareup.okhttp.internal.http.StatusLine; 43 import java.io.FileNotFoundException; 44 import java.io.IOException; 45 import java.io.InputStream; 46 import java.io.OutputStream; 47 import java.net.HttpRetryException; 48 import java.net.HttpURLConnection; 49 import java.net.InetSocketAddress; 50 import java.net.MalformedURLException; 51 import java.net.ProtocolException; 52 import java.net.Proxy; 53 import java.net.SocketPermission; 54 import java.net.URL; 55 import java.net.UnknownHostException; 56 import java.security.Permission; 57 import java.util.ArrayList; 58 import java.util.Arrays; 59 import java.util.Collections; 60 import java.util.Date; 61 import java.util.LinkedHashSet; 62 import java.util.List; 63 import java.util.Map; 64 import java.util.Set; 65 import java.util.concurrent.TimeUnit; 66 import okio.BufferedSink; 67 import okio.Sink; 68 69 /** 70 * This implementation uses HttpEngine to send requests and receive responses. 71 * This class may use multiple HttpEngines to follow redirects, authentication 72 * retries, etc. to retrieve the final response body. 73 * 74 * <h3>What does 'connected' mean?</h3> 75 * This class inherits a {@code connected} field from the superclass. That field 76 * is <strong>not</strong> used to indicate not whether this URLConnection is 77 * currently connected. Instead, it indicates whether a connection has ever been 78 * attempted. Once a connection has been attempted, certain properties (request 79 * header fields, request method, etc.) are immutable. 80 */ 81 public class HttpURLConnectionImpl extends HttpURLConnection { 82 private static final Set<String> METHODS = new LinkedHashSet<>( 83 Arrays.asList("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "PATCH")); 84 private static final RequestBody EMPTY_REQUEST_BODY = RequestBody.create(null, new byte[0]); 85 86 final OkHttpClient client; 87 88 private Headers.Builder requestHeaders = new Headers.Builder(); 89 90 /** Like the superclass field of the same name, but a long and available on all platforms. */ 91 private long fixedContentLength = -1; 92 private int followUpCount; 93 protected IOException httpEngineFailure; 94 protected HttpEngine httpEngine; 95 /** Lazily created (with synthetic headers) on first call to getHeaders(). */ 96 private Headers responseHeaders; 97 98 /** 99 * The most recently attempted route. This will be null if we haven't sent a 100 * request yet, or if the response comes from a cache. 101 */ 102 private Route route; 103 104 /** 105 * The most recently received TLS handshake. This will be null if we haven't 106 * connected yet, or if the most recent connection was HTTP (and not HTTPS). 107 */ 108 Handshake handshake; 109 110 private URLFilter urlFilter; 111 112 public HttpURLConnectionImpl(URL url, OkHttpClient client) { 113 super(url); 114 this.client = client; 115 } 116 117 public HttpURLConnectionImpl(URL url, OkHttpClient client, URLFilter urlFilter) { 118 this(url, client); 119 this.urlFilter = urlFilter; 120 } 121 122 @Override public final void connect() throws IOException { 123 initHttpEngine(); 124 boolean success; 125 do { 126 success = execute(false); 127 } while (!success); 128 } 129 130 @Override public final void disconnect() { 131 // Calling disconnect() before a connection exists should have no effect. 132 if (httpEngine == null) return; 133 134 httpEngine.disconnect(); 135 136 // This doesn't close the stream because doing so would require all stream 137 // access to be synchronized. It's expected that the thread using the 138 // connection will close its streams directly. If it doesn't, the worst 139 // case is that the GzipSource's Inflater won't be released until it's 140 // finalized. (This logs a warning on Android.) 141 } 142 143 /** 144 * Returns an input stream from the server in the case of error such as the 145 * requested file (txt, htm, html) is not found on the remote server. 146 */ 147 @Override public final InputStream getErrorStream() { 148 try { 149 HttpEngine response = getResponse(); 150 if (HttpEngine.hasBody(response.getResponse()) 151 && response.getResponse().code() >= HTTP_BAD_REQUEST) { 152 return response.getResponse().body().byteStream(); 153 } 154 return null; 155 } catch (IOException e) { 156 return null; 157 } 158 } 159 160 private Headers getHeaders() throws IOException { 161 if (responseHeaders == null) { 162 Response response = getResponse().getResponse(); 163 Headers headers = response.headers(); 164 165 responseHeaders = headers.newBuilder() 166 .add(Platform.get().getPrefix() + "-Response-Source", responseSourceHeader(response)) 167 .build(); 168 } 169 return responseHeaders; 170 } 171 172 private static String responseSourceHeader(Response response) { 173 if (response.networkResponse() == null) { 174 if (response.cacheResponse() == null) { 175 return "NONE"; 176 } 177 return "CACHE " + response.code(); 178 } 179 if (response.cacheResponse() == null) { 180 return "NETWORK " + response.code(); 181 } 182 return "CONDITIONAL_CACHE " + response.networkResponse().code(); 183 } 184 185 /** 186 * Returns the value of the field at {@code position}. Returns null if there 187 * are fewer than {@code position} headers. 188 */ 189 @Override public final String getHeaderField(int position) { 190 try { 191 return getHeaders().value(position); 192 } catch (IOException e) { 193 return null; 194 } 195 } 196 197 /** 198 * Returns the value of the field corresponding to the {@code fieldName}, or 199 * null if there is no such field. If the field has multiple values, the 200 * last value is returned. 201 */ 202 @Override public final String getHeaderField(String fieldName) { 203 try { 204 return fieldName == null 205 ? StatusLine.get(getResponse().getResponse()).toString() 206 : getHeaders().get(fieldName); 207 } catch (IOException e) { 208 return null; 209 } 210 } 211 212 @Override public final String getHeaderFieldKey(int position) { 213 try { 214 return getHeaders().name(position); 215 } catch (IOException e) { 216 return null; 217 } 218 } 219 220 @Override public final Map<String, List<String>> getHeaderFields() { 221 try { 222 return OkHeaders.toMultimap(getHeaders(), 223 StatusLine.get(getResponse().getResponse()).toString()); 224 } catch (IOException e) { 225 return Collections.emptyMap(); 226 } 227 } 228 229 @Override public final Map<String, List<String>> getRequestProperties() { 230 if (connected) { 231 throw new IllegalStateException( 232 "Cannot access request header fields after connection is set"); 233 } 234 235 return OkHeaders.toMultimap(requestHeaders.build(), null); 236 } 237 238 @Override public final InputStream getInputStream() throws IOException { 239 if (!doInput) { 240 throw new ProtocolException("This protocol does not support input"); 241 } 242 243 HttpEngine response = getResponse(); 244 245 // if the requested file does not exist, throw an exception formerly the 246 // Error page from the server was returned if the requested file was 247 // text/html this has changed to return FileNotFoundException for all 248 // file types 249 if (getResponseCode() >= HTTP_BAD_REQUEST) { 250 throw new FileNotFoundException(url.toString()); 251 } 252 253 return response.getResponse().body().byteStream(); 254 } 255 256 @Override public final OutputStream getOutputStream() throws IOException { 257 connect(); 258 259 BufferedSink sink = httpEngine.getBufferedRequestBody(); 260 if (sink == null) { 261 throw new ProtocolException("method does not support a request body: " + method); 262 } else if (httpEngine.hasResponse()) { 263 throw new ProtocolException("cannot write request body after response has been read"); 264 } 265 266 return sink.outputStream(); 267 } 268 269 @Override public final Permission getPermission() throws IOException { 270 URL url = getURL(); 271 String hostName = url.getHost(); 272 int hostPort = url.getPort() != -1 273 ? url.getPort() 274 : HttpUrl.defaultPort(url.getProtocol()); 275 if (usingProxy()) { 276 InetSocketAddress proxyAddress = (InetSocketAddress) client.getProxy().address(); 277 hostName = proxyAddress.getHostName(); 278 hostPort = proxyAddress.getPort(); 279 } 280 return new SocketPermission(hostName + ":" + hostPort, "connect, resolve"); 281 } 282 283 @Override public final String getRequestProperty(String field) { 284 if (field == null) return null; 285 return requestHeaders.get(field); 286 } 287 288 @Override public void setConnectTimeout(int timeoutMillis) { 289 client.setConnectTimeout(timeoutMillis, TimeUnit.MILLISECONDS); 290 } 291 292 @Override 293 public void setInstanceFollowRedirects(boolean followRedirects) { 294 client.setFollowRedirects(followRedirects); 295 } 296 297 @Override public boolean getInstanceFollowRedirects() { 298 return client.getFollowRedirects(); 299 } 300 301 @Override public int getConnectTimeout() { 302 return client.getConnectTimeout(); 303 } 304 305 @Override public void setReadTimeout(int timeoutMillis) { 306 client.setReadTimeout(timeoutMillis, TimeUnit.MILLISECONDS); 307 } 308 309 @Override public int getReadTimeout() { 310 return client.getReadTimeout(); 311 } 312 313 private void initHttpEngine() throws IOException { 314 if (httpEngineFailure != null) { 315 throw httpEngineFailure; 316 } else if (httpEngine != null) { 317 return; 318 } 319 320 connected = true; 321 try { 322 if (doOutput) { 323 if (method.equals("GET")) { 324 // they are requesting a stream to write to. This implies a POST method 325 method = "POST"; 326 } else if (!HttpMethod.permitsRequestBody(method)) { 327 throw new ProtocolException(method + " does not support writing"); 328 } 329 } 330 // If the user set content length to zero, we know there will not be a request body. 331 httpEngine = newHttpEngine(method, null, null, null); 332 } catch (IOException e) { 333 httpEngineFailure = e; 334 throw e; 335 } 336 } 337 338 private HttpEngine newHttpEngine(String method, Connection connection, RetryableSink requestBody, 339 Response priorResponse) throws MalformedURLException, UnknownHostException { 340 // OkHttp's Call API requires a placeholder body; the real body will be streamed separately. 341 RequestBody placeholderBody = HttpMethod.requiresRequestBody(method) 342 ? EMPTY_REQUEST_BODY 343 : null; 344 URL url = getURL(); 345 HttpUrl httpUrl = Internal.instance.getHttpUrlChecked(url.toString()); 346 Request.Builder builder = new Request.Builder() 347 .url(httpUrl) 348 .method(method, placeholderBody); 349 Headers headers = requestHeaders.build(); 350 for (int i = 0, size = headers.size(); i < size; i++) { 351 builder.addHeader(headers.name(i), headers.value(i)); 352 } 353 354 boolean bufferRequestBody = false; 355 if (HttpMethod.permitsRequestBody(method)) { 356 // Specify how the request body is terminated. 357 if (fixedContentLength != -1) { 358 builder.header("Content-Length", Long.toString(fixedContentLength)); 359 } else if (chunkLength > 0) { 360 builder.header("Transfer-Encoding", "chunked"); 361 } else { 362 bufferRequestBody = true; 363 } 364 365 // Add a content type for the request body, if one isn't already present. 366 if (headers.get("Content-Type") == null) { 367 builder.header("Content-Type", "application/x-www-form-urlencoded"); 368 } 369 } 370 371 if (headers.get("User-Agent") == null) { 372 builder.header("User-Agent", defaultUserAgent()); 373 } 374 375 Request request = builder.build(); 376 377 // If we're currently not using caches, make sure the engine's client doesn't have one. 378 OkHttpClient engineClient = client; 379 if (Internal.instance.internalCache(engineClient) != null && !getUseCaches()) { 380 engineClient = client.clone().setCache(null); 381 } 382 383 return new HttpEngine(engineClient, request, bufferRequestBody, true, false, connection, null, 384 requestBody, priorResponse); 385 } 386 387 private String defaultUserAgent() { 388 String agent = System.getProperty("http.agent"); 389 return agent != null ? Util.toHumanReadableAscii(agent) : Version.userAgent(); 390 } 391 392 /** 393 * Aggressively tries to get the final HTTP response, potentially making 394 * many HTTP requests in the process in order to cope with redirects and 395 * authentication. 396 */ 397 private HttpEngine getResponse() throws IOException { 398 initHttpEngine(); 399 400 if (httpEngine.hasResponse()) { 401 return httpEngine; 402 } 403 404 while (true) { 405 if (!execute(true)) { 406 continue; 407 } 408 409 Response response = httpEngine.getResponse(); 410 Request followUp = httpEngine.followUpRequest(); 411 412 if (followUp == null) { 413 httpEngine.releaseConnection(); 414 return httpEngine; 415 } 416 417 if (++followUpCount > HttpEngine.MAX_FOLLOW_UPS) { 418 throw new ProtocolException("Too many follow-up requests: " + followUpCount); 419 } 420 421 // The first request was insufficient. Prepare for another... 422 url = followUp.url(); 423 requestHeaders = followUp.headers().newBuilder(); 424 425 // Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM redirect 426 // should keep the same method, Chrome, Firefox and the RI all issue GETs 427 // when following any redirect. 428 Sink requestBody = httpEngine.getRequestBody(); 429 if (!followUp.method().equals(method)) { 430 requestBody = null; 431 } 432 433 if (requestBody != null && !(requestBody instanceof RetryableSink)) { 434 throw new HttpRetryException("Cannot retry streamed HTTP body", responseCode); 435 } 436 437 if (!httpEngine.sameConnection(followUp.httpUrl())) { 438 httpEngine.releaseConnection(); 439 } 440 441 Connection connection = httpEngine.close(); 442 httpEngine = newHttpEngine(followUp.method(), connection, (RetryableSink) requestBody, 443 response); 444 } 445 } 446 447 /** 448 * Sends a request and optionally reads a response. Returns true if the 449 * request was successfully executed, and false if the request can be 450 * retried. Throws an exception if the request failed permanently. 451 */ 452 private boolean execute(boolean readResponse) throws IOException { 453 if (urlFilter != null) { 454 urlFilter.checkURLPermitted(httpEngine.getRequest().url()); 455 } 456 try { 457 httpEngine.sendRequest(); 458 route = httpEngine.getRoute(); 459 handshake = httpEngine.getConnection() != null 460 ? httpEngine.getConnection().getHandshake() 461 : null; 462 if (readResponse) { 463 httpEngine.readResponse(); 464 } 465 466 return true; 467 } catch (RequestException e) { 468 // An attempt to interpret a request failed. 469 IOException toThrow = e.getCause(); 470 httpEngineFailure = toThrow; 471 throw toThrow; 472 } catch (RouteException e) { 473 // The attempt to connect via a route failed. The request will not have been sent. 474 HttpEngine retryEngine = httpEngine.recover(e); 475 if (retryEngine != null) { 476 httpEngine = retryEngine; 477 return false; 478 } 479 480 // Give up; recovery is not possible. 481 IOException toThrow = e.getLastConnectException(); 482 httpEngineFailure = toThrow; 483 throw toThrow; 484 } catch (IOException e) { 485 // An attempt to communicate with a server failed. The request may have been sent. 486 HttpEngine retryEngine = httpEngine.recover(e); 487 if (retryEngine != null) { 488 httpEngine = retryEngine; 489 return false; 490 } 491 492 // Give up; recovery is not possible. 493 httpEngineFailure = e; 494 throw e; 495 } 496 } 497 498 /** 499 * Returns true if either: 500 * <ul> 501 * <li>A specific proxy was explicitly configured for this connection. 502 * <li>The response has already been retrieved, and a proxy was {@link 503 * java.net.ProxySelector selected} in order to get it. 504 * </ul> 505 * 506 * <p><strong>Warning:</strong> This method may return false before attempting 507 * to connect and true afterwards. 508 */ 509 @Override public final boolean usingProxy() { 510 Proxy proxy = route != null 511 ? route.getProxy() 512 : client.getProxy(); 513 return proxy != null && proxy.type() != Proxy.Type.DIRECT; 514 } 515 516 @Override public String getResponseMessage() throws IOException { 517 return getResponse().getResponse().message(); 518 } 519 520 @Override public final int getResponseCode() throws IOException { 521 return getResponse().getResponse().code(); 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 // TODO: Deprecate use of X-Android-Transports header? 542 if ("X-Android-Transports".equals(field) || "X-Android-Protocols".equals(field)) { 543 setProtocols(newValue, false /* append */); 544 } else { 545 requestHeaders.set(field, newValue); 546 } 547 } 548 549 @Override public void setIfModifiedSince(long newValue) { 550 super.setIfModifiedSince(newValue); 551 if (ifModifiedSince != 0) { 552 requestHeaders.set("If-Modified-Since", HttpDate.format(new Date(ifModifiedSince))); 553 } else { 554 requestHeaders.removeAll("If-Modified-Since"); 555 } 556 } 557 558 @Override public final void addRequestProperty(String field, String value) { 559 if (connected) { 560 throw new IllegalStateException("Cannot add request property after connection is made"); 561 } 562 if (field == null) { 563 throw new NullPointerException("field == null"); 564 } 565 if (value == null) { 566 // Silently ignore null header values for backwards compatibility with older 567 // android versions as well as with other URLConnection implementations. 568 // 569 // Some implementations send a malformed HTTP header when faced with 570 // such requests, we respect the spec and ignore the header. 571 Platform.get().logW("Ignoring header " + field + " because its value was null."); 572 return; 573 } 574 575 // TODO: Deprecate use of X-Android-Transports header? 576 if ("X-Android-Transports".equals(field) || "X-Android-Protocols".equals(field)) { 577 setProtocols(value, true /* append */); 578 } else { 579 requestHeaders.add(field, value); 580 } 581 } 582 583 /* 584 * Splits and validates a comma-separated string of protocols. 585 * When append == false, we require that the transport list contains "http/1.1". 586 * Throws {@link IllegalStateException} when one of the protocols isn't 587 * defined in {@link Protocol OkHttp's protocol enumeration}. 588 */ 589 private void setProtocols(String protocolsString, boolean append) { 590 List<Protocol> protocolsList = new ArrayList<>(); 591 if (append) { 592 protocolsList.addAll(client.getProtocols()); 593 } 594 for (String protocol : protocolsString.split(",", -1)) { 595 try { 596 protocolsList.add(Protocol.get(protocol)); 597 } catch (IOException e) { 598 throw new IllegalStateException(e); 599 } 600 } 601 client.setProtocols(protocolsList); 602 } 603 604 @Override public void setRequestMethod(String method) throws ProtocolException { 605 if (!METHODS.contains(method)) { 606 throw new ProtocolException("Expected one of " + METHODS + " but was " + method); 607 } 608 this.method = method; 609 } 610 611 @Override public void setFixedLengthStreamingMode(int contentLength) { 612 setFixedLengthStreamingMode((long) contentLength); 613 } 614 615 @Override public void setFixedLengthStreamingMode(long contentLength) { 616 if (super.connected) throw new IllegalStateException("Already connected"); 617 if (chunkLength > 0) throw new IllegalStateException("Already in chunked mode"); 618 if (contentLength < 0) throw new IllegalArgumentException("contentLength < 0"); 619 this.fixedContentLength = contentLength; 620 super.fixedContentLength = (int) Math.min(contentLength, Integer.MAX_VALUE); 621 } 622 } 623