HttpURLConnectionImpl.java revision 54cf3446000fdcf88a9e62724f7deb0282e98da1
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.ConnectionPool; 22import com.squareup.okhttp.internal.Util; 23import java.io.FileNotFoundException; 24import java.io.IOException; 25import java.io.InputStream; 26import java.io.OutputStream; 27import java.net.CookieHandler; 28import java.net.HttpRetryException; 29import java.net.HttpURLConnection; 30import java.net.InetSocketAddress; 31import java.net.ProtocolException; 32import java.net.Proxy; 33import java.net.ProxySelector; 34import java.net.ResponseCache; 35import java.net.SocketPermission; 36import java.net.URL; 37import java.security.Permission; 38import java.security.cert.CertificateException; 39import java.util.List; 40import java.util.Map; 41import javax.net.ssl.SSLHandshakeException; 42 43import static com.squareup.okhttp.internal.Util.getEffectivePort; 44 45/** 46 * This implementation uses HttpEngine to send requests and receive responses. 47 * This class may use multiple HttpEngines to follow redirects, authentication 48 * retries, etc. to retrieve the final response body. 49 * 50 * <h3>What does 'connected' mean?</h3> 51 * This class inherits a {@code connected} field from the superclass. That field 52 * is <strong>not</strong> used to indicate not whether this URLConnection is 53 * currently connected. Instead, it indicates whether a connection has ever been 54 * attempted. Once a connection has been attempted, certain properties (request 55 * header fields, request method, etc.) are immutable. Test the {@code 56 * connection} field on this class for null/non-null to determine of an instance 57 * is currently connected to a server. 58 */ 59public class HttpURLConnectionImpl extends HttpURLConnection { 60 /** 61 * How many redirects should we follow? Chrome follows 21; Firefox, curl, 62 * and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5. 63 */ 64 private static final int MAX_REDIRECTS = 20; 65 66 private final int defaultPort; 67 68 private Proxy proxy; 69 final ProxySelector proxySelector; 70 final CookieHandler cookieHandler; 71 final ResponseCache responseCache; 72 final ConnectionPool connectionPool; 73 74 private final RawHeaders rawRequestHeaders = new RawHeaders(); 75 76 private int redirectionCount; 77 78 protected IOException httpEngineFailure; 79 protected HttpEngine httpEngine; 80 81 public HttpURLConnectionImpl(URL url, int defaultPort, Proxy proxy, ProxySelector proxySelector, 82 CookieHandler cookieHandler, ResponseCache responseCache, ConnectionPool connectionPool) { 83 super(url); 84 this.defaultPort = defaultPort; 85 this.proxy = proxy; 86 this.proxySelector = proxySelector; 87 this.cookieHandler = cookieHandler; 88 this.responseCache = responseCache; 89 this.connectionPool = connectionPool; 90 } 91 92 @Override public final void connect() throws IOException { 93 initHttpEngine(); 94 boolean success; 95 do { 96 success = execute(false); 97 } while (!success); 98 } 99 100 @Override public final void disconnect() { 101 // Calling disconnect() before a connection exists should have no effect. 102 if (httpEngine != null) { 103 // We close the response body here instead of in 104 // HttpEngine.release because that is called when input 105 // has been completely read from the underlying socket. 106 // However the response body can be a GZIPInputStream that 107 // still has unread data. 108 if (httpEngine.hasResponse()) { 109 Util.closeQuietly(httpEngine.getResponseBody()); 110 } 111 httpEngine.release(true); 112 } 113 } 114 115 /** 116 * Returns an input stream from the server in the case of error such as the 117 * requested file (txt, htm, html) is not found on the remote server. 118 */ 119 @Override public final InputStream getErrorStream() { 120 try { 121 HttpEngine response = getResponse(); 122 if (response.hasResponseBody() && response.getResponseCode() >= HTTP_BAD_REQUEST) { 123 return response.getResponseBody(); 124 } 125 return null; 126 } catch (IOException e) { 127 return null; 128 } 129 } 130 131 /** 132 * Returns the value of the field at {@code position}. Returns null if there 133 * are fewer than {@code position} headers. 134 */ 135 @Override public final String getHeaderField(int position) { 136 try { 137 return getResponse().getResponseHeaders().getHeaders().getValue(position); 138 } catch (IOException e) { 139 return null; 140 } 141 } 142 143 /** 144 * Returns the value of the field corresponding to the {@code fieldName}, or 145 * null if there is no such field. If the field has multiple values, the 146 * last value is returned. 147 */ 148 @Override public final String getHeaderField(String fieldName) { 149 try { 150 RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders(); 151 return fieldName == null ? rawHeaders.getStatusLine() : rawHeaders.get(fieldName); 152 } catch (IOException e) { 153 return null; 154 } 155 } 156 157 @Override public final String getHeaderFieldKey(int position) { 158 try { 159 return getResponse().getResponseHeaders().getHeaders().getFieldName(position); 160 } catch (IOException e) { 161 return null; 162 } 163 } 164 165 @Override public final Map<String, List<String>> getHeaderFields() { 166 try { 167 return getResponse().getResponseHeaders().getHeaders().toMultimap(true); 168 } catch (IOException e) { 169 return null; 170 } 171 } 172 173 @Override public final Map<String, List<String>> getRequestProperties() { 174 if (connected) { 175 throw new IllegalStateException( 176 "Cannot access request header fields after connection is set"); 177 } 178 return rawRequestHeaders.toMultimap(false); 179 } 180 181 @Override public final InputStream getInputStream() throws IOException { 182 if (!doInput) { 183 throw new ProtocolException("This protocol does not support input"); 184 } 185 186 HttpEngine response = getResponse(); 187 188 // if the requested file does not exist, throw an exception formerly the 189 // Error page from the server was returned if the requested file was 190 // text/html this has changed to return FileNotFoundException for all 191 // file types 192 if (getResponseCode() >= HTTP_BAD_REQUEST) { 193 throw new FileNotFoundException(url.toString()); 194 } 195 196 InputStream result = response.getResponseBody(); 197 if (result == null) { 198 throw new ProtocolException("No response body exists; responseCode=" + getResponseCode()); 199 } 200 return result; 201 } 202 203 @Override public final OutputStream getOutputStream() throws IOException { 204 connect(); 205 206 OutputStream result = httpEngine.getRequestBody(); 207 if (result == null) { 208 throw new ProtocolException("method does not support a request body: " + method); 209 } else if (httpEngine.hasResponse()) { 210 throw new ProtocolException("cannot write request body after response has been read"); 211 } 212 213 return result; 214 } 215 216 @Override public final Permission getPermission() throws IOException { 217 String connectToAddress = getConnectToHost() + ":" + getConnectToPort(); 218 return new SocketPermission(connectToAddress, "connect, resolve"); 219 } 220 221 private String getConnectToHost() { 222 return usingProxy() ? ((InetSocketAddress) proxy.address()).getHostName() : getURL().getHost(); 223 } 224 225 private int getConnectToPort() { 226 int hostPort = 227 usingProxy() ? ((InetSocketAddress) proxy.address()).getPort() : getURL().getPort(); 228 return hostPort < 0 ? getDefaultPort() : hostPort; 229 } 230 231 @Override public final String getRequestProperty(String field) { 232 if (field == null) { 233 return null; 234 } 235 return rawRequestHeaders.get(field); 236 } 237 238 private void initHttpEngine() throws IOException { 239 if (httpEngineFailure != null) { 240 throw httpEngineFailure; 241 } else if (httpEngine != null) { 242 return; 243 } 244 245 connected = true; 246 try { 247 if (doOutput) { 248 if (method.equals("GET")) { 249 // they are requesting a stream to write to. This implies a POST method 250 method = "POST"; 251 } else if (!method.equals("POST") && !method.equals("PUT")) { 252 // If the request method is neither POST nor PUT, then you're not writing 253 throw new ProtocolException(method + " does not support writing"); 254 } 255 } 256 httpEngine = newHttpEngine(method, rawRequestHeaders, null, null); 257 } catch (IOException e) { 258 httpEngineFailure = e; 259 throw e; 260 } 261 } 262 263 /** 264 * Create a new HTTP engine. This hook method is non-final so it can be 265 * overridden by HttpsURLConnectionImpl. 266 */ 267 protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders, 268 Connection connection, RetryableOutputStream requestBody) throws IOException { 269 return new HttpEngine(this, method, requestHeaders, connection, requestBody); 270 } 271 272 /** 273 * Aggressively tries to get the final HTTP response, potentially making 274 * many HTTP requests in the process in order to cope with redirects and 275 * authentication. 276 */ 277 private HttpEngine getResponse() throws IOException { 278 initHttpEngine(); 279 280 if (httpEngine.hasResponse()) { 281 return httpEngine; 282 } 283 284 while (true) { 285 if (!execute(true)) { 286 continue; 287 } 288 289 Retry retry = processResponseHeaders(); 290 if (retry == Retry.NONE) { 291 httpEngine.automaticallyReleaseConnectionToPool(); 292 return httpEngine; 293 } 294 295 // The first request was insufficient. Prepare for another... 296 String retryMethod = method; 297 OutputStream requestBody = httpEngine.getRequestBody(); 298 299 // Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM 300 // redirect should keep the same method, Chrome, Firefox and the 301 // RI all issue GETs when following any redirect. 302 int responseCode = getResponseCode(); 303 if (responseCode == HTTP_MULT_CHOICE 304 || responseCode == HTTP_MOVED_PERM 305 || responseCode == HTTP_MOVED_TEMP 306 || responseCode == HTTP_SEE_OTHER) { 307 retryMethod = "GET"; 308 requestBody = null; 309 } 310 311 if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) { 312 throw new HttpRetryException("Cannot retry streamed HTTP body", 313 httpEngine.getResponseCode()); 314 } 315 316 if (retry == Retry.DIFFERENT_CONNECTION) { 317 httpEngine.automaticallyReleaseConnectionToPool(); 318 } 319 320 httpEngine.release(false); 321 322 httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(), 323 (RetryableOutputStream) requestBody); 324 } 325 } 326 327 /** 328 * Sends a request and optionally reads a response. Returns true if the 329 * request was successfully executed, and false if the request can be 330 * retried. Throws an exception if the request failed permanently. 331 */ 332 private boolean execute(boolean readResponse) throws IOException { 333 try { 334 httpEngine.sendRequest(); 335 if (readResponse) { 336 httpEngine.readResponse(); 337 } 338 return true; 339 } catch (IOException e) { 340 RouteSelector routeSelector = httpEngine.routeSelector; 341 if (routeSelector != null && httpEngine.connection != null) { 342 routeSelector.connectFailed(httpEngine.connection, e); 343 } 344 if (routeSelector == null && httpEngine.connection == null) { 345 throw e; // If we failed before finding a route or a connection, give up. 346 } 347 348 // The connection failure isn't fatal if there's another route to attempt. 349 OutputStream requestBody = httpEngine.getRequestBody(); 350 if ((routeSelector == null || routeSelector.hasNext()) && isRecoverable(e) && (requestBody 351 == null || requestBody instanceof RetryableOutputStream)) { 352 httpEngine.release(true); 353 httpEngine = 354 newHttpEngine(method, rawRequestHeaders, null, (RetryableOutputStream) requestBody); 355 httpEngine.routeSelector = routeSelector; // Keep the same routeSelector. 356 return false; 357 } 358 httpEngineFailure = e; 359 throw e; 360 } 361 } 362 363 private boolean isRecoverable(IOException e) { 364 // If the problem was a CertificateException from the X509TrustManager, 365 // do not retry, we didn't have an abrupt server initiated exception. 366 boolean sslFailure = 367 e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException; 368 boolean protocolFailure = e instanceof ProtocolException; 369 return !sslFailure && !protocolFailure; 370 } 371 372 HttpEngine getHttpEngine() { 373 return httpEngine; 374 } 375 376 enum Retry { 377 NONE, 378 SAME_CONNECTION, 379 DIFFERENT_CONNECTION 380 } 381 382 /** 383 * Returns the retry action to take for the current response headers. The 384 * headers, proxy and target URL or this connection may be adjusted to 385 * prepare for a follow up request. 386 */ 387 private Retry processResponseHeaders() throws IOException { 388 switch (getResponseCode()) { 389 case HTTP_PROXY_AUTH: 390 if (!usingProxy()) { 391 throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy"); 392 } 393 // fall-through 394 case HTTP_UNAUTHORIZED: 395 boolean credentialsFound = HttpAuthenticator.processAuthHeader(getResponseCode(), 396 httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, proxy, url); 397 return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE; 398 399 case HTTP_MULT_CHOICE: 400 case HTTP_MOVED_PERM: 401 case HTTP_MOVED_TEMP: 402 case HTTP_SEE_OTHER: 403 if (!getInstanceFollowRedirects()) { 404 return Retry.NONE; 405 } 406 if (++redirectionCount > MAX_REDIRECTS) { 407 throw new ProtocolException("Too many redirects: " + redirectionCount); 408 } 409 String location = getHeaderField("Location"); 410 if (location == null) { 411 return Retry.NONE; 412 } 413 URL previousUrl = url; 414 url = new URL(previousUrl, location); 415 if (!previousUrl.getProtocol().equals(url.getProtocol())) { 416 return Retry.NONE; // the scheme changed; don't retry. 417 } 418 if (previousUrl.getHost().equals(url.getHost()) 419 && getEffectivePort(previousUrl) == getEffectivePort(url)) { 420 return Retry.SAME_CONNECTION; 421 } else { 422 return Retry.DIFFERENT_CONNECTION; 423 } 424 425 default: 426 return Retry.NONE; 427 } 428 } 429 430 final int getDefaultPort() { 431 return defaultPort; 432 } 433 434 /** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */ 435 final int getFixedContentLength() { 436 return fixedContentLength; 437 } 438 439 /** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */ 440 final int getChunkLength() { 441 return chunkLength; 442 } 443 444 final Proxy getProxy() { 445 return proxy; 446 } 447 448 final void setProxy(Proxy proxy) { 449 this.proxy = proxy; 450 } 451 452 @Override public final boolean usingProxy() { 453 return (proxy != null && proxy.type() != Proxy.Type.DIRECT); 454 } 455 456 @Override public String getResponseMessage() throws IOException { 457 return getResponse().getResponseHeaders().getHeaders().getResponseMessage(); 458 } 459 460 @Override public final int getResponseCode() throws IOException { 461 return getResponse().getResponseCode(); 462 } 463 464 @Override public final void setRequestProperty(String field, String newValue) { 465 if (connected) { 466 throw new IllegalStateException("Cannot set request property after connection is made"); 467 } 468 if (field == null) { 469 throw new NullPointerException("field == null"); 470 } 471 rawRequestHeaders.set(field, newValue); 472 } 473 474 @Override public final void addRequestProperty(String field, String value) { 475 if (connected) { 476 throw new IllegalStateException("Cannot add request property after connection is made"); 477 } 478 if (field == null) { 479 throw new NullPointerException("field == null"); 480 } 481 rawRequestHeaders.add(field, value); 482 } 483} 484