Home | History | Annotate | Download | only in repository
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.sdklib.internal.repository;
     18 
     19 import com.android.util.Pair;
     20 
     21 import org.apache.http.HttpEntity;
     22 import org.apache.http.HttpResponse;
     23 import org.apache.http.HttpStatus;
     24 import org.apache.http.auth.AuthScope;
     25 import org.apache.http.auth.AuthState;
     26 import org.apache.http.auth.Credentials;
     27 import org.apache.http.auth.UsernamePasswordCredentials;
     28 import org.apache.http.client.ClientProtocolException;
     29 import org.apache.http.client.methods.HttpGet;
     30 import org.apache.http.client.protocol.ClientContext;
     31 import org.apache.http.impl.client.DefaultHttpClient;
     32 import org.apache.http.impl.conn.ProxySelectorRoutePlanner;
     33 import org.apache.http.protocol.BasicHttpContext;
     34 import org.apache.http.protocol.HttpContext;
     35 
     36 import java.io.FileNotFoundException;
     37 import java.io.FilterInputStream;
     38 import java.io.IOException;
     39 import java.io.InputStream;
     40 import java.net.ProxySelector;
     41 import java.net.URL;
     42 import java.util.HashMap;
     43 import java.util.Map;
     44 
     45 /**
     46  * This class holds methods for adding URLs management.
     47  * @see #openUrl(String, ITaskMonitor)
     48  */
     49 public class UrlOpener {
     50 
     51     public static class CanceledByUserException extends Exception {
     52         private static final long serialVersionUID = -7669346110926032403L;
     53 
     54         public CanceledByUserException(String message) {
     55             super(message);
     56         }
     57     }
     58 
     59     private static Map<String, Pair<String, String>> sRealmCache =
     60             new HashMap<String, Pair<String, String>>();
     61 
     62     /**
     63      * Opens a URL. It can be a simple URL or one which requires basic
     64      * authentication.
     65      * <p/>
     66      * Tries to access the given URL. If http response is either
     67      * {@code HttpStatus.SC_UNAUTHORIZED} or
     68      * {@code HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED}, asks for
     69      * login/password and tries to authenticate into proxy server and/or URL.
     70      * <p/>
     71      * This implementation relies on the Apache Http Client due to its
     72      * capabilities of proxy/http authentication. <br/>
     73      * Proxy configuration is determined by {@link ProxySelectorRoutePlanner} using the JVM proxy
     74      * settings by default.
     75      * <p/>
     76      * For more information see: <br/>
     77      * - {@code http://hc.apache.org/httpcomponents-client-ga/} <br/>
     78      * - {@code http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/conn/ProxySelectorRoutePlanner.html}
     79      * <p/>
     80      * There's a very simple <b>cache</b> implementation.
     81      * Login/Password for each realm are stored in a static {@link Map}.
     82      * Before asking the user the method verifies if the information is already available in cache.
     83      *
     84      * @param url the URL string to be opened.
     85      * @param monitor {@link ITaskMonitor} which is related to this URL
     86      *            fetching.
     87      * @return Returns an {@link InputStream} holding the URL content.
     88      * @throws IOException Exception thrown when there are problems retrieving
     89      *             the URL or its content.
     90      * @throws CanceledByUserException Exception thrown if the user cancels the
     91      *              authentication dialog.
     92      */
     93     static InputStream openUrl(String url, ITaskMonitor monitor)
     94         throws IOException, CanceledByUserException {
     95 
     96         try {
     97             return openWithHttpClient(url, monitor);
     98 
     99         } catch (ClientProtocolException e) {
    100             // If the protocol is not supported by HttpClient (e.g. file:///),
    101             // revert to the standard java.net.Url.open
    102 
    103             URL u = new URL(url);
    104             return u.openStream();
    105         }
    106     }
    107 
    108     private static InputStream openWithHttpClient(String url, ITaskMonitor monitor)
    109             throws IOException, ClientProtocolException, CanceledByUserException {
    110         Pair<String, String> result = null;
    111         String realm = null;
    112 
    113         // use the simple one
    114         final DefaultHttpClient httpClient = new DefaultHttpClient();
    115 
    116         // create local execution context
    117         HttpContext localContext = new BasicHttpContext();
    118         HttpGet httpget = new HttpGet(url);
    119 
    120         // retrieve local java configured network in case there is the need to
    121         // authenticate a proxy
    122         ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner(
    123                     httpClient.getConnectionManager().getSchemeRegistry(),
    124                     ProxySelector.getDefault());
    125         httpClient.setRoutePlanner(routePlanner);
    126 
    127         boolean trying = true;
    128         // loop while the response is being fetched
    129         while (trying) {
    130             // connect and get status code
    131             HttpResponse response = httpClient.execute(httpget, localContext);
    132             int statusCode = response.getStatusLine().getStatusCode();
    133 
    134             // check whether any authentication is required
    135             AuthState authenticationState = null;
    136             if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
    137                 // Target host authentication required
    138                 authenticationState = (AuthState) localContext
    139                         .getAttribute(ClientContext.TARGET_AUTH_STATE);
    140             }
    141             if (statusCode == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) {
    142                 // Proxy authentication required
    143                 authenticationState = (AuthState) localContext
    144                         .getAttribute(ClientContext.PROXY_AUTH_STATE);
    145             }
    146             if (statusCode == HttpStatus.SC_OK) {
    147                 // in case the status is OK and there is a realm and result,
    148                 // cache it
    149                 if (realm != null && result != null) {
    150                     sRealmCache.put(realm, result);
    151                 }
    152             }
    153 
    154             // there is the need for authentication
    155             if (authenticationState != null) {
    156 
    157                 // get scope and realm
    158                 AuthScope authScope = authenticationState.getAuthScope();
    159 
    160                 // If the current realm is different from the last one it means
    161                 // a pass was performed successfully to the last URL, therefore
    162                 // cache the last realm
    163                 if (realm != null && !realm.equals(authScope.getRealm())) {
    164                     sRealmCache.put(realm, result);
    165                 }
    166 
    167                 realm = authScope.getRealm();
    168 
    169                 // in case there is cache for this Realm, use it to authenticate
    170                 if (sRealmCache.containsKey(realm)) {
    171                     result = sRealmCache.get(realm);
    172                 } else {
    173                     // since there is no cache, request for login and password
    174                     result = monitor.displayLoginPasswordPrompt("Site Authentication",
    175                             "Please login to the following domain: " + realm +
    176                             "\n\nServer requiring authentication:\n" + authScope.getHost());
    177                     if (result == null) {
    178                         throw new CanceledByUserException("User canceled login dialog.");
    179                     }
    180                 }
    181 
    182                 // retrieve authentication data
    183                 String user = result.getFirst();
    184                 String password = result.getSecond();
    185 
    186                 // proceed in case there is indeed a user
    187                 if (user != null && user.length() > 0) {
    188                     Credentials credentials = new UsernamePasswordCredentials(user, password);
    189                     httpClient.getCredentialsProvider().setCredentials(authScope, credentials);
    190                     trying = true;
    191                 } else {
    192                     trying = false;
    193                 }
    194             } else {
    195                 trying = false;
    196             }
    197 
    198             HttpEntity entity = response.getEntity();
    199 
    200             if (entity != null) {
    201                 if (trying) {
    202                     // in case another pass to the Http Client will be performed, close the entity.
    203                     entity.getContent().close();
    204                 } else {
    205                     // since no pass to the Http Client is needed, retrieve the
    206                     // entity's content.
    207 
    208                     // Note: don't use something like a BufferedHttpEntity since it would consume
    209                     // all content and store it in memory, resulting in an OutOfMemory exception
    210                     // on a large download.
    211 
    212                     return new FilterInputStream(entity.getContent()) {
    213                         @Override
    214                         public void close() throws IOException {
    215                             super.close();
    216 
    217                             // since Http Client is no longer needed, close it
    218                             httpClient.getConnectionManager().shutdown();
    219                         }
    220                     };
    221                 }
    222             }
    223         }
    224 
    225         // We get here if we did not succeed. Callers do not expect a null result.
    226         httpClient.getConnectionManager().shutdown();
    227         throw new FileNotFoundException(url);
    228     }
    229 }
    230