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 org.apache.harmony.luni.internal.net.www.protocol.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.CacheRequest; 26 import java.net.CacheResponse; 27 import java.net.CookieHandler; 28 import java.net.HttpRetryException; 29 import java.net.HttpURLConnection; 30 import java.net.InetAddress; 31 import java.net.InetSocketAddress; 32 import java.net.PasswordAuthentication; 33 import java.net.ProtocolException; 34 import java.net.Proxy; 35 import java.net.ProxySelector; 36 import java.net.ResponseCache; 37 import java.net.SocketPermission; 38 import java.net.URI; 39 import java.net.URISyntaxException; 40 import java.net.URL; 41 import java.nio.charset.Charsets; 42 import java.security.AccessController; 43 import java.security.Permission; 44 import java.security.PrivilegedAction; 45 import java.text.SimpleDateFormat; 46 import java.util.Date; 47 import java.util.List; 48 import java.util.Locale; 49 import java.util.Map; 50 import java.util.TimeZone; 51 import java.util.zip.GZIPInputStream; 52 import libcore.base.Streams; 53 import org.apache.harmony.luni.util.Base64; 54 import org.apache.harmony.luni.util.PriviAction; 55 56 /** 57 * This subclass extends <code>HttpURLConnection</code> which in turns extends 58 * <code>URLConnection</code> This is the actual class that "does the work", 59 * such as connecting, sending request and getting the content from the remote 60 * server. 61 * 62 * <h3>What does 'connected' mean?</h3> 63 * This class inherits a {@code connected} field from the superclass. That field 64 * is <strong>not</strong> used to indicate not whether this URLConnection is 65 * currently connected. Instead, it indicates whether a connection has ever been 66 * attempted. Once a connection has been attempted, certain properties (request 67 * headers, request method, etc.) are immutable. Test the {@code connection} 68 * field on this class for null/non-null to determine of an instance is 69 * currently connected to a server. 70 */ 71 public class HttpURLConnectionImpl extends HttpURLConnection { 72 public static final String OPTIONS = "OPTIONS"; 73 public static final String GET = "GET"; 74 public static final String HEAD = "HEAD"; 75 public static final String POST = "POST"; 76 public static final String PUT = "PUT"; 77 public static final String DELETE = "DELETE"; 78 public static final String TRACE = "TRACE"; 79 public static final String CONNECT = "CONNECT"; 80 81 public static final int HTTP_CONTINUE = 100; 82 83 /** 84 * HTTP 1.1 doesn't specify how many redirects to follow, but HTTP/1.0 85 * recommended 5. http://www.w3.org/Protocols/HTTP/1.0/spec.html#Code3xx 86 */ 87 public static final int MAX_REDIRECTS = 5; 88 89 /** 90 * The subset of HTTP methods that the user may select via {@link #setRequestMethod}. 91 */ 92 public static String PERMITTED_USER_METHODS[] = { 93 OPTIONS, 94 GET, 95 HEAD, 96 POST, 97 PUT, 98 DELETE, 99 TRACE 100 // Note: we don't allow users to specify "CONNECT" 101 }; 102 103 public static final int DEFAULT_CHUNK_LENGTH = 1024; 104 105 private final int defaultPort; 106 107 /** 108 * The version this client will use. Either 0 for HTTP/1.0, or 1 for 109 * HTTP/1.1. Upon receiving a non-HTTP/1.1 response, this client 110 * automatically sets its version to HTTP/1.0. 111 */ 112 private int httpVersion = 1; // Assume HTTP/1.1 113 114 protected HttpConnection connection; 115 private InputStream socketIn; 116 private OutputStream socketOut; 117 118 private InputStream responseBodyIn; 119 private AbstractHttpOutputStream requestBodyOut; 120 121 private ResponseCache responseCache; 122 123 private CacheResponse cacheResponse; 124 125 private CacheRequest cacheRequest; 126 127 private boolean hasTriedCache; 128 129 private boolean sentRequestHeaders; 130 131 /** 132 * True if this client added an "Accept-Encoding: gzip" header and is 133 * therefore responsible for also decompressing the transfer stream. 134 */ 135 private boolean transparentGzip = false; 136 137 boolean sendChunked; 138 139 // proxy which is used to make the connection. 140 private Proxy proxy; 141 142 // the destination URI 143 private URI uri; 144 145 private static Header defaultRequestHeader = new Header(); 146 147 private final Header requestHeader; 148 149 /** Null until a response is received from the network or the cache */ 150 private Header responseHeader; 151 152 private int redirectionCount; 153 154 /** 155 * Intermediate responses are always followed by another request for the 156 * same content, possibly from a different URL or with different headers. 157 */ 158 protected boolean intermediateResponse = false; 159 160 /** 161 * Creates an instance of the <code>HttpURLConnection</code> 162 * 163 * @param url 164 * URL The URL this connection is connecting 165 * @param port 166 * int The default connection port 167 */ 168 protected HttpURLConnectionImpl(URL url, int port) { 169 super(url); 170 defaultPort = port; 171 requestHeader = (Header) defaultRequestHeader.clone(); 172 173 try { 174 uri = url.toURI(); 175 } catch (URISyntaxException e) { 176 // do nothing. 177 } 178 responseCache = AccessController 179 .doPrivileged(new PrivilegedAction<ResponseCache>() { 180 public ResponseCache run() { 181 return ResponseCache.getDefault(); 182 } 183 }); 184 } 185 186 /** 187 * Creates an instance of the <code>HttpURLConnection</code> 188 * 189 * @param url 190 * URL The URL this connection is connecting 191 * @param port 192 * int The default connection port 193 * @param proxy 194 * Proxy The proxy which is used to make the connection 195 */ 196 protected HttpURLConnectionImpl(URL url, int port, Proxy proxy) { 197 this(url, port); 198 this.proxy = proxy; 199 } 200 201 @Override public void connect() throws IOException { 202 if (connected) { 203 return; 204 } 205 makeConnection(); 206 } 207 208 /** 209 * Internal method to open a connection to the server. Unlike connect(), 210 * this method may be called multiple times for a single response. This may 211 * be necessary when following redirects. 212 * 213 * <p>Request parameters may not be changed after this method has been 214 * called. 215 */ 216 public void makeConnection() throws IOException { 217 connected = true; 218 219 if (connection != null) { 220 return; 221 } 222 223 if (getFromCache()) { 224 return; 225 } 226 227 /* 228 * URL.toURI() throws if it has illegal characters. Since we don't mind 229 * illegal characters for proxy selection, just create the minimal URI. 230 */ 231 try { 232 uri = new URI(url.getProtocol(), null, url.getHost(), url.getPort(), url.getPath(), 233 null, null); 234 } catch (URISyntaxException e1) { 235 throw new IOException(e1.getMessage()); 236 } 237 238 // try to determine: to use the proxy or not 239 if (proxy != null) { 240 // try to make the connection to the proxy 241 // specified in constructor. 242 // IOException will be thrown in the case of failure 243 connection = getHttpConnection(proxy); 244 } else { 245 // Use system-wide ProxySelect to select proxy list, 246 // then try to connect via elements in the proxy list. 247 ProxySelector selector = ProxySelector.getDefault(); 248 List<Proxy> proxyList = selector.select(uri); 249 if (proxyList != null) { 250 for (Proxy selectedProxy : proxyList) { 251 if (selectedProxy.type() == Proxy.Type.DIRECT) { 252 // the same as NO_PROXY 253 continue; 254 } 255 try { 256 connection = getHttpConnection(selectedProxy); 257 proxy = selectedProxy; 258 break; // connected 259 } catch (IOException e) { 260 // failed to connect, tell it to the selector 261 selector.connectFailed(uri, selectedProxy.address(), e); 262 } 263 } 264 } 265 if (connection == null) { 266 // make direct connection 267 connection = getHttpConnection(null); 268 } 269 } 270 connection.setSoTimeout(getReadTimeout()); 271 setUpTransportIO(connection); 272 } 273 274 /** 275 * Returns connected socket to be used for this HTTP connection. 276 */ 277 private HttpConnection getHttpConnection(Proxy proxy) throws IOException { 278 HttpConnection.Address address; 279 if (proxy == null || proxy.type() == Proxy.Type.DIRECT) { 280 this.proxy = null; // not using proxy 281 address = new HttpConnection.Address(uri); 282 } else { 283 address = new HttpConnection.Address(uri, proxy, requiresTunnel()); 284 } 285 return HttpConnectionPool.INSTANCE.get(address, getConnectTimeout()); 286 } 287 288 /** 289 * Sets up the data streams used to send requests and read responses. 290 */ 291 protected void setUpTransportIO(HttpConnection connection) throws IOException { 292 socketOut = connection.getOutputStream(); 293 socketIn = connection.getInputStream(); 294 } 295 296 /** 297 * Returns true if the input streams are prepared to return data from the 298 * cache. 299 */ 300 private boolean getFromCache() throws IOException { 301 if (!useCaches || responseCache == null || hasTriedCache) { 302 return (hasTriedCache && socketIn != null); 303 } 304 305 hasTriedCache = true; 306 cacheResponse = responseCache.get(uri, method, requestHeader.getFieldMap()); 307 if (cacheResponse == null) { 308 return socketIn != null; // TODO: if this is non-null, why are we calling getFromCache? 309 } 310 Map<String, List<String>> headersMap = cacheResponse.getHeaders(); 311 if (headersMap != null) { 312 responseHeader = new Header(headersMap); 313 } 314 socketIn = responseBodyIn = cacheResponse.getBody(); 315 return socketIn != null; 316 } 317 318 private void maybeCache() throws IOException { 319 // Are we caching at all? 320 if (!useCaches || responseCache == null) { 321 return; 322 } 323 // Should we cache this particular response code? 324 // TODO: cache response code 300 HTTP_MULT_CHOICE ? 325 if (responseCode != HTTP_OK && responseCode != HTTP_NOT_AUTHORITATIVE && 326 responseCode != HTTP_PARTIAL && responseCode != HTTP_MOVED_PERM && 327 responseCode != HTTP_GONE) { 328 return; 329 } 330 // Offer this request to the cache. 331 cacheRequest = responseCache.put(uri, this); 332 } 333 334 /** 335 * Close the socket connection to the remote origin server or proxy. 336 */ 337 @Override public void disconnect() { 338 releaseSocket(false); 339 } 340 341 /** 342 * Releases this connection so that it may be either reused or closed. 343 */ 344 protected synchronized void releaseSocket(boolean reuseSocket) { 345 // we cannot recycle sockets that have incomplete output. 346 if (requestBodyOut != null && !requestBodyOut.closed) { 347 reuseSocket = false; 348 } 349 350 // if the headers specify that the connection shouldn't be reused, don't reuse it 351 if (hasConnectionCloseHeader()) { 352 reuseSocket = false; 353 } 354 355 /* 356 * Don't return the socket to the connection pool if this is an 357 * intermediate response; we're going to use it again right away. 358 */ 359 if (intermediateResponse && reuseSocket) { 360 return; 361 } 362 363 if (connection != null) { 364 if (reuseSocket) { 365 HttpConnectionPool.INSTANCE.recycle(connection); 366 } else { 367 connection.closeSocketAndStreams(); 368 } 369 connection = null; 370 } 371 372 /* 373 * Clear "socketIn" and "socketOut" to ensure that no further I/O 374 * attempts from this instance make their way to the underlying 375 * connection (which may get recycled). 376 */ 377 socketIn = null; 378 socketOut = null; 379 } 380 381 /** 382 * Discard all state initialized from the HTTP response including response 383 * code, message, headers and body. 384 */ 385 protected void discardIntermediateResponse() throws IOException { 386 boolean oldIntermediateResponse = intermediateResponse; 387 intermediateResponse = true; 388 try { 389 if (responseBodyIn != null) { 390 if (!(responseBodyIn instanceof UnknownLengthHttpInputStream)) { 391 // skip the response so that the connection may be reused for the retry 392 Streams.skipAll(responseBodyIn); 393 } 394 responseBodyIn.close(); 395 responseBodyIn = null; 396 } 397 sentRequestHeaders = false; 398 responseHeader = null; 399 responseCode = -1; 400 responseMessage = null; 401 cacheRequest = null; 402 } finally { 403 intermediateResponse = oldIntermediateResponse; 404 } 405 } 406 407 /** 408 * Returns an input stream from the server in the case of error such as the 409 * requested file (txt, htm, html) is not found on the remote server. 410 * <p> 411 * If the content type is not what stated above, 412 * <code>FileNotFoundException</code> is thrown. 413 * 414 * @return InputStream the error input stream returned by the server. 415 */ 416 @Override 417 public InputStream getErrorStream() { 418 if (connected && method != HEAD && responseCode >= HTTP_BAD_REQUEST) { 419 return responseBodyIn; 420 } 421 return null; 422 } 423 424 /** 425 * Returns the value of the field at position <code>pos<code>. 426 * Returns <code>null</code> if there is fewer than <code>pos</code> fields 427 * in the response header. 428 * 429 * @return java.lang.String The value of the field 430 * @param pos int the position of the field from the top 431 * 432 * @see #getHeaderField(String) 433 * @see #getHeaderFieldKey 434 */ 435 @Override 436 public String getHeaderField(int pos) { 437 try { 438 getInputStream(); 439 } catch (IOException e) { 440 // ignore 441 } 442 if (null == responseHeader) { 443 return null; 444 } 445 return responseHeader.get(pos); 446 } 447 448 /** 449 * Returns the value of the field corresponding to the <code>key</code> 450 * Returns <code>null</code> if there is no such field. 451 * 452 * If there are multiple fields with that key, the last field value is 453 * returned. 454 * 455 * @return java.lang.String The value of the header field 456 * @param key 457 * java.lang.String the name of the header field 458 * 459 * @see #getHeaderField(int) 460 * @see #getHeaderFieldKey 461 */ 462 @Override 463 public String getHeaderField(String key) { 464 try { 465 getInputStream(); 466 } catch (IOException e) { 467 // ignore 468 } 469 if (null == responseHeader) { 470 return null; 471 } 472 return responseHeader.get(key); 473 } 474 475 @Override 476 public String getHeaderFieldKey(int pos) { 477 try { 478 getInputStream(); 479 } catch (IOException e) { 480 // ignore 481 } 482 if (null == responseHeader) { 483 return null; 484 } 485 return responseHeader.getKey(pos); 486 } 487 488 @Override 489 public Map<String, List<String>> getHeaderFields() { 490 try { 491 retrieveResponse(); 492 } catch (IOException ignored) { 493 } 494 return responseHeader != null ? responseHeader.getFieldMap() : null; 495 } 496 497 @Override 498 public Map<String, List<String>> getRequestProperties() { 499 if (connected) { 500 throw new IllegalStateException( 501 "Cannot access request header fields after connection is set"); 502 } 503 return requestHeader.getFieldMap(); 504 } 505 506 @Override 507 public InputStream getInputStream() throws IOException { 508 if (!doInput) { 509 throw new ProtocolException("This protocol does not support input"); 510 } 511 512 retrieveResponse(); 513 514 /* 515 * if the requested file does not exist, throw an exception formerly the 516 * Error page from the server was returned if the requested file was 517 * text/html this has changed to return FileNotFoundException for all 518 * file types 519 */ 520 if (responseCode >= HTTP_BAD_REQUEST) { 521 throw new FileNotFoundException(url.toString()); 522 } 523 524 if (responseBodyIn == null) { 525 throw new IOException("No response body exists; responseCode=" + responseCode); 526 } 527 528 return responseBodyIn; 529 } 530 531 private InputStream initContentStream() throws IOException { 532 InputStream transferStream = getTransferStream(); 533 if (transparentGzip && "gzip".equalsIgnoreCase(responseHeader.get("Content-Encoding"))) { 534 /* 535 * If the response was transparently gzipped, remove the gzip header 536 * so clients don't double decompress. http://b/3009828 537 */ 538 responseHeader.removeAll("Content-Encoding"); 539 responseBodyIn = new GZIPInputStream(transferStream); 540 } else { 541 responseBodyIn = transferStream; 542 } 543 return responseBodyIn; 544 } 545 546 private InputStream getTransferStream() throws IOException { 547 if (!hasResponseBody()) { 548 return new FixedLengthInputStream(socketIn, cacheRequest, this, 0); 549 } 550 551 if ("chunked".equalsIgnoreCase(responseHeader.get("Transfer-Encoding"))) { 552 return new ChunkedInputStream(socketIn, cacheRequest, this); 553 } 554 555 String contentLength = responseHeader.get("Content-Length"); 556 if (contentLength != null) { 557 try { 558 int length = Integer.parseInt(contentLength); 559 return new FixedLengthInputStream(socketIn, cacheRequest, this, length); 560 } catch (NumberFormatException ignored) { 561 } 562 } 563 564 /* 565 * Wrap the input stream from the HttpConnection (rather than 566 * just returning "socketIn" directly here), so that we can control 567 * its use after the reference escapes. 568 */ 569 return new UnknownLengthHttpInputStream(socketIn, cacheRequest, this); 570 } 571 572 @Override 573 public OutputStream getOutputStream() throws IOException { 574 if (!doOutput) { 575 throw new ProtocolException("Does not support output"); 576 } 577 578 // you can't write after you read 579 if (sentRequestHeaders) { 580 // TODO: just return 'requestBodyOut' if that's non-null? 581 throw new ProtocolException( 582 "OutputStream unavailable because request headers have already been sent!"); 583 } 584 585 if (requestBodyOut != null) { 586 return requestBodyOut; 587 } 588 589 // they are requesting a stream to write to. This implies a POST method 590 if (method == GET) { 591 method = POST; 592 } 593 594 // If the request method is neither PUT or POST, then you're not writing 595 if (method != PUT && method != POST) { 596 throw new ProtocolException(method + " does not support writing"); 597 } 598 599 int contentLength = -1; 600 String contentLengthString = requestHeader.get("Content-Length"); 601 if (contentLengthString != null) { 602 contentLength = Integer.parseInt(contentLengthString); 603 } 604 605 String encoding = requestHeader.get("Transfer-Encoding"); 606 if (chunkLength > 0 || "chunked".equalsIgnoreCase(encoding)) { 607 sendChunked = true; 608 contentLength = -1; 609 if (chunkLength == -1) { 610 chunkLength = DEFAULT_CHUNK_LENGTH; 611 } 612 } 613 614 connect(); 615 616 if (socketOut == null) { 617 // TODO: what should we do if a cached response exists? 618 throw new IOException("No socket to write to; was a POST cached?"); 619 } 620 621 if (httpVersion == 0) { 622 sendChunked = false; 623 } 624 625 if (fixedContentLength != -1) { 626 writeRequestHeaders(socketOut); 627 requestBodyOut = new FixedLengthOutputStream(socketOut, fixedContentLength); 628 } else if (sendChunked) { 629 writeRequestHeaders(socketOut); 630 requestBodyOut = new ChunkedOutputStream(socketOut, chunkLength); 631 } else if (contentLength != -1) { 632 requestBodyOut = new RetryableOutputStream(contentLength); 633 } else { 634 requestBodyOut = new RetryableOutputStream(); 635 } 636 return requestBodyOut; 637 } 638 639 @Override 640 public Permission getPermission() throws IOException { 641 String connectToAddress = getConnectToHost() + ":" + getConnectToPort(); 642 return new SocketPermission(connectToAddress, "connect, resolve"); 643 } 644 645 @Override 646 public String getRequestProperty(String field) { 647 if (null == field) { 648 return null; 649 } 650 return requestHeader.get(field); 651 } 652 653 /** 654 * Returns the characters up to but not including the next "\r\n", "\n", or 655 * the end of the stream, consuming the end of line delimiter. 656 */ 657 static String readLine(InputStream is) throws IOException { 658 StringBuilder result = new StringBuilder(80); 659 while (true) { 660 int c = is.read(); 661 if (c == -1 || c == '\n') { 662 break; 663 } 664 665 result.append((char) c); 666 } 667 int length = result.length(); 668 if (length > 0 && result.charAt(length - 1) == '\r') { 669 result.setLength(length - 1); 670 } 671 return result.toString(); 672 } 673 674 protected String requestString() { 675 if (usingProxy()) { 676 return url.toString(); 677 } 678 String file = url.getFile(); 679 if (file == null || file.length() == 0) { 680 file = "/"; 681 } 682 return file; 683 } 684 685 private void readResponseHeaders() throws IOException { 686 do { 687 responseCode = -1; 688 responseMessage = null; 689 responseHeader = new Header(); 690 responseHeader.setStatusLine(readLine(socketIn).trim()); 691 readHeaders(); 692 } while (parseResponseCode() == HTTP_CONTINUE); 693 } 694 695 /** 696 * Returns true if the response must have a (possibly 0-length) body. 697 * See RFC 2616 section 4.3. 698 */ 699 private boolean hasResponseBody() { 700 if (method != HEAD 701 && method != CONNECT 702 && (responseCode < HTTP_CONTINUE || responseCode >= 200) 703 && responseCode != HTTP_NO_CONTENT 704 && responseCode != HTTP_NOT_MODIFIED) { 705 return true; 706 } 707 708 /* 709 * If the Content-Length or Transfer-Encoding headers disagree with the 710 * response code, the response is malformed. For best compatibility, we 711 * honor the headers. 712 */ 713 String contentLength = responseHeader.get("Content-Length"); 714 if (contentLength != null && Integer.parseInt(contentLength) > 0) { 715 return true; 716 } 717 if ("chunked".equalsIgnoreCase(responseHeader.get("Transfer-Encoding"))) { 718 return true; 719 } 720 721 return false; 722 } 723 724 @Override 725 public int getResponseCode() throws IOException { 726 retrieveResponse(); 727 return responseCode; 728 } 729 730 private int parseResponseCode() { 731 // Response Code Sample : "HTTP/1.0 200 OK" 732 String response = responseHeader.getStatusLine(); 733 if (response == null || !response.startsWith("HTTP/")) { 734 return -1; 735 } 736 response = response.trim(); 737 int mark = response.indexOf(" ") + 1; 738 if (mark == 0) { 739 return -1; 740 } 741 if (response.charAt(mark - 2) != '1') { 742 httpVersion = 0; 743 } 744 int last = mark + 3; 745 if (last > response.length()) { 746 last = response.length(); 747 } 748 responseCode = Integer.parseInt(response.substring(mark, last)); 749 if (last + 1 <= response.length()) { 750 responseMessage = response.substring(last + 1); 751 } 752 return responseCode; 753 } 754 755 void readHeaders() throws IOException { 756 // parse the result headers until the first blank line 757 String line; 758 while ((line = readLine(socketIn)).length() > 1) { 759 // Header parsing 760 int index = line.indexOf(":"); 761 if (index == -1) { 762 responseHeader.add("", line.trim()); 763 } else { 764 responseHeader.add(line.substring(0, index), line.substring(index + 1).trim()); 765 } 766 } 767 768 CookieHandler cookieHandler = CookieHandler.getDefault(); 769 if (cookieHandler != null) { 770 cookieHandler.put(uri, responseHeader.getFieldMap()); 771 } 772 } 773 774 /** 775 * Prepares the HTTP headers and sends them to the server. 776 * 777 * <p>For streaming requests with a body, headers must be prepared 778 * <strong>before</strong> the output stream has been written to. Otherwise 779 * the body would need to be buffered! 780 * 781 * <p>For non-streaming requests with a body, headers must be prepared 782 * <strong>after</strong> the output stream has been written to and closed. 783 * This ensures that the {@code Content-Length} header receives the proper 784 * value. 785 */ 786 private void writeRequestHeaders(OutputStream out) throws IOException { 787 Header header = prepareRequestHeaders(); 788 789 StringBuilder result = new StringBuilder(256); 790 result.append(header.getStatusLine()).append("\r\n"); 791 for (int i = 0; i < header.length(); i++) { 792 String key = header.getKey(i); 793 String value = header.get(i); 794 if (key != null) { 795 result.append(key).append(": ").append(value).append("\r\n"); 796 } 797 } 798 result.append("\r\n"); 799 out.write(result.toString().getBytes(Charsets.ISO_8859_1)); 800 sentRequestHeaders = true; 801 } 802 803 /** 804 * Populates requestHeader with the HTTP headers to be sent. Header values are 805 * derived from the request itself and the cookie manager. 806 * 807 * <p>This client doesn't specify a default {@code Accept} header because it 808 * doesn't know what content types the application is interested in. 809 */ 810 private Header prepareRequestHeaders() throws IOException { 811 /* 812 * If we're establishing an HTTPS tunnel with CONNECT (RFC 2817 5.2), 813 * send only the minimum set of headers. This avoids sending potentially 814 * sensitive data like HTTP cookies to the proxy unencrypted. 815 */ 816 if (method == CONNECT) { 817 Header proxyHeader = new Header(); 818 proxyHeader.setStatusLine(getStatusLine()); 819 820 // always set Host and User-Agent 821 String host = requestHeader.get("Host"); 822 if (host == null) { 823 host = getOriginAddress(url); 824 } 825 proxyHeader.set("Host", host); 826 827 String userAgent = requestHeader.get("User-Agent"); 828 if (userAgent == null) { 829 userAgent = getDefaultUserAgent(); 830 } 831 proxyHeader.set("User-Agent", userAgent); 832 833 // copy over the Proxy-Authorization header if it exists 834 String proxyAuthorization = requestHeader.get("Proxy-Authorization"); 835 if (proxyAuthorization != null) { 836 proxyHeader.set("Proxy-Authorization", proxyAuthorization); 837 } 838 839 // Always set the Proxy-Connection to Keep-Alive for the benefit of 840 // HTTP/1.0 proxies like Squid. 841 proxyHeader.set("Proxy-Connection", "Keep-Alive"); 842 return proxyHeader; 843 } 844 845 requestHeader.setStatusLine(getStatusLine()); 846 847 if (requestHeader.get("User-Agent") == null) { 848 requestHeader.add("User-Agent", getDefaultUserAgent()); 849 } 850 851 if (requestHeader.get("Host") == null) { 852 requestHeader.add("Host", getOriginAddress(url)); 853 } 854 855 if (httpVersion > 0) { 856 requestHeader.addIfAbsent("Connection", "Keep-Alive"); 857 } 858 859 if (fixedContentLength != -1) { 860 requestHeader.addIfAbsent("Content-Length", Integer.toString(fixedContentLength)); 861 } else if (sendChunked) { 862 requestHeader.addIfAbsent("Transfer-Encoding", "chunked"); 863 } else if (requestBodyOut instanceof RetryableOutputStream) { 864 int size = ((RetryableOutputStream) requestBodyOut).contentLength(); 865 requestHeader.addIfAbsent("Content-Length", Integer.toString(size)); 866 } 867 868 if (requestBodyOut != null) { 869 requestHeader.addIfAbsent("Content-Type", "application/x-www-form-urlencoded"); 870 } 871 872 if (requestHeader.get("Accept-Encoding") == null) { 873 transparentGzip = true; 874 requestHeader.set("Accept-Encoding", "gzip"); 875 } 876 877 CookieHandler cookieHandler = CookieHandler.getDefault(); 878 if (cookieHandler != null) { 879 Map<String, List<String>> allCookieHeaders 880 = cookieHandler.get(uri, requestHeader.getFieldMap()); 881 for (Map.Entry<String, List<String>> entry : allCookieHeaders.entrySet()) { 882 String key = entry.getKey(); 883 if ("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) { 884 requestHeader.addAll(key, entry.getValue()); 885 } 886 } 887 } 888 889 return requestHeader; 890 } 891 892 private String getStatusLine() { 893 String protocol = (httpVersion == 0) ? "HTTP/1.0" : "HTTP/1.1"; 894 return method + " " + requestString() + " " + protocol; 895 } 896 897 private String getDefaultUserAgent() { 898 String agent = getSystemProperty("http.agent"); 899 return agent != null ? agent : ("Java" + getSystemProperty("java.version")); 900 } 901 902 private boolean hasConnectionCloseHeader() { 903 return (responseHeader != null 904 && "close".equalsIgnoreCase(responseHeader.get("Connection"))) 905 || (requestHeader != null 906 && "close".equalsIgnoreCase(requestHeader.get("Connection"))); 907 } 908 909 private String getOriginAddress(URL url) { 910 int port = url.getPort(); 911 String result = url.getHost(); 912 if (port > 0 && port != defaultPort) { 913 result = result + ":" + port; 914 } 915 return result; 916 } 917 918 /** 919 * A slightly different implementation from this parent's 920 * <code>setIfModifiedSince()</code> Since this HTTP impl supports 921 * IfModifiedSince as one of the header field, the request header is updated 922 * with the new value. 923 * 924 * 925 * @param newValue 926 * the number of millisecond since epoch 927 * 928 * @throws IllegalStateException 929 * if already connected. 930 */ 931 @Override 932 public void setIfModifiedSince(long newValue) { 933 super.setIfModifiedSince(newValue); 934 // convert from millisecond since epoch to date string 935 SimpleDateFormat sdf = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US); 936 sdf.setTimeZone(TimeZone.getTimeZone("GMT")); 937 String date = sdf.format(new Date(newValue)); 938 requestHeader.add("If-Modified-Since", date); 939 } 940 941 @Override 942 public void setRequestProperty(String field, String newValue) { 943 if (connected) { 944 throw new IllegalStateException("Cannot set request property after connection is made"); 945 } 946 if (field == null) { 947 throw new NullPointerException(); 948 } 949 requestHeader.set(field, newValue); 950 } 951 952 @Override 953 public void addRequestProperty(String field, String value) { 954 if (connected) { 955 throw new IllegalStateException("Cannot set request property after connection is made"); 956 } 957 if (field == null) { 958 throw new NullPointerException(); 959 } 960 requestHeader.add(field, value); 961 } 962 963 /** 964 * Returns the target port of the socket connection; either a port of the 965 * origin server or an intermediate proxy. 966 */ 967 private int getConnectToPort() { 968 int hostPort = usingProxy() 969 ? ((InetSocketAddress) proxy.address()).getPort() 970 : url.getPort(); 971 return hostPort < 0 ? defaultPort : hostPort; 972 } 973 974 /** 975 * Returns the target address of the socket connection; either the address 976 * of the origin server or an intermediate proxy. 977 */ 978 private InetAddress getConnectToInetAddress() throws IOException { 979 return usingProxy() 980 ? ((InetSocketAddress) proxy.address()).getAddress() 981 : InetAddress.getByName(url.getHost()); 982 } 983 984 /** 985 * Returns the target host name of the socket connection; either the host 986 * name of the origin server or an intermediate proxy. 987 */ 988 private String getConnectToHost() { 989 return usingProxy() 990 ? ((InetSocketAddress) proxy.address()).getHostName() 991 : url.getHost(); 992 } 993 994 private String getSystemProperty(final String property) { 995 return AccessController.doPrivileged(new PriviAction<String>(property)); 996 } 997 998 @Override public final boolean usingProxy() { 999 return (proxy != null && proxy.type() != Proxy.Type.DIRECT); 1000 } 1001 1002 protected boolean requiresTunnel() { 1003 return false; 1004 } 1005 1006 /** 1007 * Aggressively tries to get the final HTTP response, potentially making 1008 * many HTTP requests in the process in order to cope with redirects and 1009 * authentication. 1010 */ 1011 protected final void retrieveResponse() throws IOException { 1012 if (responseHeader != null) { 1013 return; 1014 } 1015 1016 redirectionCount = 0; 1017 while (true) { 1018 makeConnection(); 1019 1020 // if we can get a response from the cache, we're done 1021 if (cacheResponse != null) { 1022 // TODO: how does this interact with redirects? Consider processing the headers so 1023 // that a redirect is never returned. 1024 return; 1025 } 1026 1027 if (!sentRequestHeaders) { 1028 writeRequestHeaders(socketOut); 1029 } 1030 1031 if (requestBodyOut != null) { 1032 requestBodyOut.close(); 1033 if (requestBodyOut instanceof RetryableOutputStream) { 1034 ((RetryableOutputStream) requestBodyOut).writeToSocket(socketOut); 1035 } 1036 } 1037 1038 socketOut.flush(); 1039 1040 readResponseHeaders(); 1041 1042 if (hasResponseBody()) { 1043 maybeCache(); // reentrant. this calls into user code which may call back into this! 1044 } 1045 1046 initContentStream(); 1047 1048 Retry retry = processResponseHeaders(); 1049 1050 if (retry == Retry.NONE) { 1051 return; 1052 } 1053 1054 /* 1055 * The first request wasn't sufficient. Prepare for another... 1056 */ 1057 1058 if (requestBodyOut != null && !(requestBodyOut instanceof RetryableOutputStream)) { 1059 throw new HttpRetryException("Cannot retry streamed HTTP body", responseCode); 1060 } 1061 1062 if (retry == Retry.SAME_CONNECTION && hasConnectionCloseHeader()) { 1063 retry = Retry.NEW_CONNECTION; 1064 } 1065 1066 discardIntermediateResponse(); 1067 1068 if (retry == Retry.NEW_CONNECTION) { 1069 releaseSocket(true); 1070 } 1071 } 1072 } 1073 1074 enum Retry { 1075 NONE, 1076 SAME_CONNECTION, 1077 NEW_CONNECTION 1078 } 1079 1080 /** 1081 * Returns the retry action to take for the current response headers. The 1082 * headers, proxy and target URL or this connection may be adjusted to 1083 * prepare for a follow up request. 1084 */ 1085 private Retry processResponseHeaders() throws IOException { 1086 switch (responseCode) { 1087 case HTTP_PROXY_AUTH: // proxy authorization failed ? 1088 if (!usingProxy()) { 1089 throw new IOException( 1090 "Received HTTP_PROXY_AUTH (407) code while not using proxy"); 1091 } 1092 return processAuthHeader("Proxy-Authenticate", "Proxy-Authorization"); 1093 1094 case HTTP_UNAUTHORIZED: // HTTP authorization failed ? 1095 return processAuthHeader("WWW-Authenticate", "Authorization"); 1096 1097 case HTTP_MULT_CHOICE: 1098 case HTTP_MOVED_PERM: 1099 case HTTP_MOVED_TEMP: 1100 case HTTP_SEE_OTHER: 1101 case HTTP_USE_PROXY: 1102 if (!getInstanceFollowRedirects()) { 1103 return Retry.NONE; 1104 } 1105 if (requestBodyOut != null) { 1106 // TODO: follow redirects for retryable output streams... 1107 return Retry.NONE; 1108 } 1109 if (++redirectionCount > MAX_REDIRECTS) { 1110 throw new ProtocolException("Too many redirects"); 1111 } 1112 String location = getHeaderField("Location"); 1113 if (location == null) { 1114 return Retry.NONE; 1115 } 1116 if (responseCode == HTTP_USE_PROXY) { 1117 int start = 0; 1118 if (location.startsWith(url.getProtocol() + ':')) { 1119 start = url.getProtocol().length() + 1; 1120 } 1121 if (location.startsWith("//", start)) { 1122 start += 2; 1123 } 1124 setProxy(location.substring(start)); 1125 return Retry.NEW_CONNECTION; 1126 } 1127 URL previousUrl = url; 1128 url = new URL(previousUrl, location); 1129 if (!previousUrl.getProtocol().equals(url.getProtocol())) { 1130 return Retry.NONE; // the scheme changed; don't retry. 1131 } 1132 if (previousUrl.getHost().equals(url.getHost()) 1133 && previousUrl.getEffectivePort() == url.getEffectivePort()) { 1134 return Retry.SAME_CONNECTION; 1135 } else { 1136 // TODO: strip cookies? 1137 requestHeader.removeAll("Host"); 1138 return Retry.NEW_CONNECTION; 1139 } 1140 1141 default: 1142 return Retry.NONE; 1143 } 1144 } 1145 1146 /** 1147 * React to a failed authorization response by looking up new credentials. 1148 */ 1149 private Retry processAuthHeader(String responseHeader, String retryHeader) throws IOException { 1150 // keep asking for username/password until authorized 1151 String challenge = this.responseHeader.get(responseHeader); 1152 if (challenge == null) { 1153 throw new IOException("Received authentication challenge is null"); 1154 } 1155 String credentials = getAuthorizationCredentials(challenge); 1156 if (credentials == null) { 1157 return Retry.NONE; // could not find credentials, end request cycle 1158 } 1159 // add authorization credentials, bypassing the already-connected check 1160 requestHeader.set(retryHeader, credentials); 1161 return Retry.SAME_CONNECTION; 1162 } 1163 1164 /** 1165 * Returns the authorization credentials on the base of provided challenge. 1166 */ 1167 private String getAuthorizationCredentials(String challenge) throws IOException { 1168 int idx = challenge.indexOf(" "); 1169 if (idx == -1) { 1170 return null; 1171 } 1172 String scheme = challenge.substring(0, idx); 1173 int realm = challenge.indexOf("realm=\"") + 7; 1174 String prompt = null; 1175 if (realm != -1) { 1176 int end = challenge.indexOf('"', realm); 1177 if (end != -1) { 1178 prompt = challenge.substring(realm, end); 1179 } 1180 } 1181 // use the global authenticator to get the password 1182 PasswordAuthentication pa = Authenticator.requestPasswordAuthentication( 1183 getConnectToInetAddress(), getConnectToPort(), url.getProtocol(), prompt, scheme); 1184 if (pa == null) { 1185 return null; 1186 } 1187 // base64 encode the username and password 1188 String usernameAndPassword = pa.getUserName() + ":" + new String(pa.getPassword()); 1189 byte[] bytes = usernameAndPassword.getBytes(Charsets.ISO_8859_1); 1190 String encoded = Base64.encode(bytes, Charsets.ISO_8859_1); 1191 return scheme + " " + encoded; 1192 } 1193 1194 private void setProxy(String proxy) { 1195 // TODO: convert IllegalArgumentException etc. to ProtocolException? 1196 int colon = proxy.indexOf(':'); 1197 String host; 1198 int port; 1199 if (colon != -1) { 1200 host = proxy.substring(0, colon); 1201 port = Integer.parseInt(proxy.substring(colon + 1)); 1202 } else { 1203 host = proxy; 1204 port = defaultPort; 1205 } 1206 this.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port)); 1207 } 1208 } 1209