Home | History | Annotate | Download | only in http
      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