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