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