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 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