• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.annotations.NonNull;
20 import com.android.annotations.Nullable;
21 import com.android.util.Pair;
22 
23 import org.apache.http.Header;
24 import org.apache.http.HttpEntity;
25 import org.apache.http.HttpResponse;
26 import org.apache.http.HttpStatus;
27 import org.apache.http.ProtocolVersion;
28 import org.apache.http.auth.AuthScope;
29 import org.apache.http.auth.AuthState;
30 import org.apache.http.auth.Credentials;
31 import org.apache.http.auth.NTCredentials;
32 import org.apache.http.auth.params.AuthPNames;
33 import org.apache.http.client.ClientProtocolException;
34 import org.apache.http.client.methods.HttpGet;
35 import org.apache.http.client.params.AuthPolicy;
36 import org.apache.http.client.protocol.ClientContext;
37 import org.apache.http.impl.client.DefaultHttpClient;
38 import org.apache.http.impl.conn.ProxySelectorRoutePlanner;
39 import org.apache.http.message.BasicHttpResponse;
40 import org.apache.http.protocol.BasicHttpContext;
41 import org.apache.http.protocol.HttpContext;
42 
43 import java.io.FileNotFoundException;
44 import java.io.FilterInputStream;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.net.ProxySelector;
48 import java.net.URL;
49 import java.util.ArrayList;
50 import java.util.HashMap;
51 import java.util.List;
52 import java.util.Map;
53 
54 /**
55  * This class holds methods for adding URLs management.
56  * @see #openUrl(String, ITaskMonitor, Header[])
57  */
58 public class UrlOpener {
59 
60     public static class CanceledByUserException extends Exception {
61         private static final long serialVersionUID = -7669346110926032403L;
62 
CanceledByUserException(String message)63         public CanceledByUserException(String message) {
64             super(message);
65         }
66     }
67 
68     private static Map<String, UserCredentials> sRealmCache =
69             new HashMap<String, UserCredentials>();
70 
71     /**
72      * Opens a URL. It can be a simple URL or one which requires basic
73      * authentication.
74      * <p/>
75      * Tries to access the given URL. If http response is either
76      * {@code HttpStatus.SC_UNAUTHORIZED} or
77      * {@code HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED}, asks for
78      * login/password and tries to authenticate into proxy server and/or URL.
79      * <p/>
80      * This implementation relies on the Apache Http Client due to its
81      * capabilities of proxy/http authentication. <br/>
82      * Proxy configuration is determined by {@link ProxySelectorRoutePlanner} using the JVM proxy
83      * settings by default.
84      * <p/>
85      * For more information see: <br/>
86      * - {@code http://hc.apache.org/httpcomponents-client-ga/} <br/>
87      * - {@code http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/conn/ProxySelectorRoutePlanner.html}
88      * <p/>
89      * There's a very simple realm cache implementation.
90      * Login/Password for each realm are stored in a static {@link Map}.
91      * Before asking the user the method verifies if the information is already
92      * available in the memory cache.
93      *
94      * @param url the URL string to be opened.
95      * @param monitor {@link ITaskMonitor} which is related to this URL
96      *            fetching.
97      * @param headers An optional array of HTTP headers to use in the GET request.
98      * @return Returns an {@link InputStream} holding the URL content and
99      *      the HttpResponse (locale, headers and an status line).
100      *      This never returns null; an exception is thrown instead in case of
101      *      error or if the user canceled an authentication dialog.
102      * @throws IOException Exception thrown when there are problems retrieving
103      *             the URL or its content.
104      * @throws CanceledByUserException Exception thrown if the user cancels the
105      *              authentication dialog.
106      */
openUrl( @onNull String url, @NonNull ITaskMonitor monitor, @Nullable Header[] headers)107     static @NonNull Pair<InputStream, HttpResponse> openUrl(
108             @NonNull String url,
109             @NonNull ITaskMonitor monitor,
110             @Nullable Header[] headers)
111         throws IOException, CanceledByUserException {
112 
113         try {
114             return openWithHttpClient(url, monitor, headers);
115 
116         } catch (ClientProtocolException e) {
117             // If the protocol is not supported by HttpClient (e.g. file:///),
118             // revert to the standard java.net.Url.open
119 
120             URL u = new URL(url);
121             InputStream is = u.openStream();
122             HttpResponse response = new BasicHttpResponse(
123                     new ProtocolVersion(u.getProtocol(), 1, 0),
124                     200, "");
125             return Pair.of(is, response);
126         }
127     }
128 
openWithHttpClient( @onNull String url, @NonNull ITaskMonitor monitor, Header[] headers)129     private static @NonNull Pair<InputStream, HttpResponse> openWithHttpClient(
130             @NonNull String url,
131             @NonNull ITaskMonitor monitor,
132             Header[] headers)
133             throws IOException, ClientProtocolException, CanceledByUserException {
134         UserCredentials result = null;
135         String realm = null;
136 
137         // use the simple one
138         final DefaultHttpClient httpClient = new DefaultHttpClient();
139 
140         // create local execution context
141         HttpContext localContext = new BasicHttpContext();
142         final HttpGet httpGet = new HttpGet(url);
143         if (headers != null) {
144             for (Header header : headers) {
145                 httpGet.addHeader(header);
146             }
147         }
148 
149         // retrieve local java configured network in case there is the need to
150         // authenticate a proxy
151         ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner(
152                     httpClient.getConnectionManager().getSchemeRegistry(),
153                     ProxySelector.getDefault());
154         httpClient.setRoutePlanner(routePlanner);
155 
156         // Set preference order for authentication options.
157         // In particular, we don't add AuthPolicy.SPNEGO, which is given preference over NTLM in
158         // servers that support both, as it is more secure. However, we don't seem to handle it
159         // very well, so we leave it off the list.
160         // See http://hc.apache.org/httpcomponents-client-ga/tutorial/html/authentication.html for
161         // more info.
162         List<String> authpref = new ArrayList<String>();
163         authpref.add(AuthPolicy.BASIC);
164         authpref.add(AuthPolicy.DIGEST);
165         authpref.add(AuthPolicy.NTLM);
166         httpClient.getParams().setParameter(AuthPNames.PROXY_AUTH_PREF, authpref);
167         httpClient.getParams().setParameter(AuthPNames.TARGET_AUTH_PREF, authpref);
168 
169         boolean trying = true;
170         // loop while the response is being fetched
171         while (trying) {
172             // connect and get status code
173             HttpResponse response = httpClient.execute(httpGet, localContext);
174             int statusCode = response.getStatusLine().getStatusCode();
175 
176             // check whether any authentication is required
177             AuthState authenticationState = null;
178             if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
179                 // Target host authentication required
180                 authenticationState = (AuthState) localContext
181                         .getAttribute(ClientContext.TARGET_AUTH_STATE);
182             }
183             if (statusCode == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) {
184                 // Proxy authentication required
185                 authenticationState = (AuthState) localContext
186                         .getAttribute(ClientContext.PROXY_AUTH_STATE);
187             }
188             if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NOT_MODIFIED) {
189                 // in case the status is OK and there is a realm and result,
190                 // cache it
191                 if (realm != null && result != null) {
192                     sRealmCache.put(realm, result);
193                 }
194             }
195 
196             // there is the need for authentication
197             if (authenticationState != null) {
198 
199                 // get scope and realm
200                 AuthScope authScope = authenticationState.getAuthScope();
201 
202                 // If the current realm is different from the last one it means
203                 // a pass was performed successfully to the last URL, therefore
204                 // cache the last realm
205                 if (realm != null && !realm.equals(authScope.getRealm())) {
206                     sRealmCache.put(realm, result);
207                 }
208 
209                 realm = authScope.getRealm();
210 
211                 // in case there is cache for this Realm, use it to authenticate
212                 if (sRealmCache.containsKey(realm)) {
213                     result = sRealmCache.get(realm);
214                 } else {
215                     // since there is no cache, request for login and password
216                     result = monitor.displayLoginCredentialsPrompt("Site Authentication",
217                             "Please login to the following domain: " + realm +
218                             "\n\nServer requiring authentication:\n" + authScope.getHost());
219                     if (result == null) {
220                         throw new CanceledByUserException("User canceled login dialog.");
221                     }
222                 }
223 
224                 // retrieve authentication data
225                 String user = result.getUserName();
226                 String password = result.getPassword();
227                 String workstation = result.getWorkstation();
228                 String domain = result.getDomain();
229 
230                 // proceed in case there is indeed a user
231                 if (user != null && user.length() > 0) {
232                     Credentials credentials = new NTCredentials(user, password,
233                             workstation, domain);
234                     httpClient.getCredentialsProvider().setCredentials(authScope, credentials);
235                     trying = true;
236                 } else {
237                     trying = false;
238                 }
239             } else {
240                 trying = false;
241             }
242 
243             HttpEntity entity = response.getEntity();
244 
245             if (entity != null) {
246                 if (trying) {
247                     // in case another pass to the Http Client will be performed, close the entity.
248                     entity.getContent().close();
249                 } else {
250                     // since no pass to the Http Client is needed, retrieve the
251                     // entity's content.
252 
253                     // Note: don't use something like a BufferedHttpEntity since it would consume
254                     // all content and store it in memory, resulting in an OutOfMemory exception
255                     // on a large download.
256                     InputStream is = new FilterInputStream(entity.getContent()) {
257                         @Override
258                         public void close() throws IOException {
259                             // Since Http Client is no longer needed, close it.
260 
261                             // Bug #21167: we need to tell http client to shutdown
262                             // first, otherwise the super.close() would continue
263                             // downloading and not return till complete.
264 
265                             httpClient.getConnectionManager().shutdown();
266                             super.close();
267                         }
268                     };
269 
270                     HttpResponse outResponse = new BasicHttpResponse(response.getStatusLine());
271                     outResponse.setHeaders(response.getAllHeaders());
272                     outResponse.setLocale(response.getLocale());
273 
274                     return Pair.of(is, outResponse);
275                 }
276             } else if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
277                 // It's ok to not have an entity (e.g. nothing to download) for a 304
278                 HttpResponse outResponse = new BasicHttpResponse(response.getStatusLine());
279                 outResponse.setHeaders(response.getAllHeaders());
280                 outResponse.setLocale(response.getLocale());
281 
282                 return Pair.of(null, outResponse);
283             }
284         }
285 
286         // We get here if we did not succeed. Callers do not expect a null result.
287         httpClient.getConnectionManager().shutdown();
288         throw new FileNotFoundException(url);
289     }
290 }
291