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