/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.sdklib.internal.repository; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.utils.Pair; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.ProtocolVersion; import org.apache.http.auth.AuthScope; import org.apache.http.auth.AuthState; import org.apache.http.auth.Credentials; import org.apache.http.auth.NTCredentials; import org.apache.http.auth.params.AuthPNames; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.params.AuthPolicy; import org.apache.http.client.protocol.ClientContext; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.ProxySelectorRoutePlanner; import org.apache.http.message.BasicHttpResponse; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.HttpContext; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.Proxy; import java.net.ProxySelector; import java.net.URI; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; /** * This class holds static methods for downloading URL resources. * @see #openUrl(String, boolean, ITaskMonitor, Header[]) * <p/> * Implementation detail: callers should use {@link DownloadCache} instead of this class. * {@link DownloadCache#openDirectUrl} is a direct pass-through to {@link UrlOpener} since * there's no caching. However from an implementation perspective it's still recommended * to pass down a {@link DownloadCache} instance, which will let us override the implementation * later on (for testing, for example.) * * @deprecated * com.android.sdklib.internal.repository has moved into Studio as * com.android.tools.idea.sdk.remote.internal. */ @Deprecated class UrlOpener { private static final boolean DEBUG = System.getenv("ANDROID_DEBUG_URL_OPENER") != null; //$NON-NLS-1$ private static Map<String, UserCredentials> sRealmCache = new HashMap<String, UserCredentials>(); /** Timeout to establish a connection, in milliseconds. */ private static int sConnectionTimeoutMs; /** Timeout waiting for data on a socket, in milliseconds. */ private static int sSocketTimeoutMs; static { if (DEBUG) { Properties props = System.getProperties(); for (String key : new String[] { "http.proxyHost", //$NON-NLS-1$ "http.proxyPort", //$NON-NLS-1$ "https.proxyHost", //$NON-NLS-1$ "https.proxyPort" }) { //$NON-NLS-1$ String prop = props.getProperty(key); if (prop != null) { System.out.printf( "SdkLib.UrlOpener Java.Prop %s='%s'\n", //$NON-NLS-1$ key, prop); } } } try { sConnectionTimeoutMs = Integer.parseInt(System.getenv("ANDROID_SDKMAN_CONN_TIMEOUT")); } catch (Exception ignore) { sConnectionTimeoutMs = 2 * 60 * 1000; } try { sSocketTimeoutMs = Integer.parseInt(System.getenv("ANDROID_SDKMAN_READ_TIMEOUT")); } catch (Exception ignore) { sSocketTimeoutMs = 1 * 60 * 1000; } } /** * This class cannot be instantiated. * @see #openUrl(String, boolean, ITaskMonitor, Header[]) */ private UrlOpener() { } /** * Opens a URL. It can be a simple URL or one which requires basic * authentication. * <p/> * Tries to access the given URL. If http response is either * {@code HttpStatus.SC_UNAUTHORIZED} or * {@code HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED}, asks for * login/password and tries to authenticate into proxy server and/or URL. * <p/> * This implementation relies on the Apache Http Client due to its * capabilities of proxy/http authentication. <br/> * Proxy configuration is determined by {@link ProxySelectorRoutePlanner} using the JVM proxy * settings by default. * <p/> * For more information see: <br/> * - {@code http://hc.apache.org/httpcomponents-client-ga/} <br/> * - {@code http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/conn/ProxySelectorRoutePlanner.html} * <p/> * There's a very simple realm cache implementation. * Login/Password for each realm are stored in a static {@link Map}. * Before asking the user the method verifies if the information is already * available in the memory cache. * * @param url the URL string to be opened. * @param needsMarkResetSupport Indicates the caller <em>must</em> have an input stream that * supports the mark/reset operations (as indicated by {@link InputStream#markSupported()}. * Implementation detail: If the original stream does not, it will be fetched and wrapped * into a {@link ByteArrayInputStream}. This can only work sanely if the resource is a * small file that can fit in memory. It also means the caller has no chance of showing * a meaningful download progress. If unsure, callers should set this to false. * @param monitor {@link ITaskMonitor} to output status. * @param headers An optional array of HTTP headers to use in the GET request. * @return Returns a {@link Pair} with {@code first} holding an {@link InputStream} * and {@code second} holding an {@link HttpResponse}. * The returned pair is never null and contains * at least a code; for http requests that provide them the response * also contains locale, headers and an status line. * The input stream can be null, especially in case of error. * The caller must only accept the stream if the response code is 200 or similar. * @throws IOException Exception thrown when there are problems retrieving * the URL or its content. * @throws CanceledByUserException Exception thrown if the user cancels the * authentication dialog. */ @NonNull static Pair<InputStream, HttpResponse> openUrl( @NonNull String url, boolean needsMarkResetSupport, @NonNull ITaskMonitor monitor, @Nullable Header[] headers) throws IOException, CanceledByUserException { Exception fallbackOnJavaUrlConnect = null; Pair<InputStream, HttpResponse> result = null; try { result = openWithHttpClient(url, monitor, headers); } catch (UnknownHostException e) { // Host in unknown. No need to even retry with the Url object, // if it's broken, it's broken. It's already an IOException but // it could use a better message. throw new IOException("Unknown Host " + e.getMessage(), e); } catch (ClientProtocolException e) { // We get this when HttpClient fails to accept the current protocol, // e.g. when processing file:// URLs. fallbackOnJavaUrlConnect = e; } catch (IOException e) { throw e; } catch (CanceledByUserException e) { // HTTP Basic Auth or NTLM login was canceled by user. throw e; } catch (Exception e) { if (DEBUG) { System.out.printf("[HttpClient Error] %s : %s\n", url, e.toString()); } fallbackOnJavaUrlConnect = e; } if (fallbackOnJavaUrlConnect != null) { // If the protocol is not supported by HttpClient (e.g. file:///), // revert to the standard java.net.Url.open. try { result = openWithUrl(url, headers); } catch (IOException e) { throw e; } catch (Exception e) { if (DEBUG && !fallbackOnJavaUrlConnect.equals(e)) { System.out.printf("[Url Error] %s : %s\n", url, e.toString()); } } } // If the caller requires an InputStream that supports mark/reset, let's // make sure we have such a stream. if (result != null && needsMarkResetSupport) { InputStream is = result.getFirst(); if (is != null) { if (!is.markSupported()) { try { // Consume the whole input stream and offer a byte array stream instead. // This can only work sanely if the resource is a small file that can // fit in memory. It also means the caller has no chance of showing // a meaningful download progress. InputStream is2 = toByteArrayInputStream(is); if (is2 != null) { result = Pair.of(is2, result.getSecond()); try { is.close(); } catch (Exception ignore) {} } } catch (Exception e3) { // Ignore. If this can't work, caller will fail later. } } } } if (result == null) { // Make up an error code if we don't have one already. HttpResponse outResponse = new BasicHttpResponse( new ProtocolVersion("HTTP", 1, 0), //$NON-NLS-1$ HttpStatus.SC_METHOD_FAILURE, ""); //$NON-NLS-1$; // 420=Method Failure result = Pair.of(null, outResponse); } return result; } // ByteArrayInputStream is the duct tape of input streams. private static InputStream toByteArrayInputStream(InputStream is) throws IOException { int inc = 4096; int curr = 0; byte[] result = new byte[inc]; int n; while ((n = is.read(result, curr, result.length - curr)) != -1) { curr += n; if (curr == result.length) { byte[] temp = new byte[curr + inc]; System.arraycopy(result, 0, temp, 0, curr); result = temp; } } return new ByteArrayInputStream(result, 0, curr); } private static Pair<InputStream, HttpResponse> openWithUrl( String url, Header[] inHeaders) throws IOException { URL u = new URL(url); URLConnection c = u.openConnection(); c.setConnectTimeout(sConnectionTimeoutMs); c.setReadTimeout(sSocketTimeoutMs); if (inHeaders != null) { for (Header header : inHeaders) { c.setRequestProperty(header.getName(), header.getValue()); } } // Trigger the access to the resource // (at which point setRequestProperty can't be used anymore.) int code = 200; if (c instanceof HttpURLConnection) { code = ((HttpURLConnection) c).getResponseCode(); } // Get the input stream. That can fail for a file:// that doesn't exist // in which case we set the response code to 404. // Also we need a buffered input stream since the caller need to use is.reset(). InputStream is = null; try { is = new BufferedInputStream(c.getInputStream()); } catch (Exception ignore) { if (is == null && code == 200) { code = 404; } } HttpResponse outResponse = new BasicHttpResponse( new ProtocolVersion(u.getProtocol(), 1, 0), // make up the protocol version code, ""); //$NON-NLS-1$; Map<String, List<String>> outHeaderMap = c.getHeaderFields(); for (Entry<String, List<String>> entry : outHeaderMap.entrySet()) { String name = entry.getKey(); if (name != null) { List<String> values = entry.getValue(); if (!values.isEmpty()) { outResponse.setHeader(name, values.get(0)); } } } return Pair.of(is, outResponse); } @NonNull private static Pair<InputStream, HttpResponse> openWithHttpClient( @NonNull String url, @NonNull ITaskMonitor monitor, Header[] inHeaders) throws IOException, ClientProtocolException, CanceledByUserException { UserCredentials result = null; String realm = null; HttpParams params = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(params, sConnectionTimeoutMs); HttpConnectionParams.setSoTimeout(params, sSocketTimeoutMs); // use the simple one final DefaultHttpClient httpClient = new DefaultHttpClient(params); // create local execution context HttpContext localContext = new BasicHttpContext(); final HttpGet httpGet = new HttpGet(url); if (inHeaders != null) { for (Header header : inHeaders) { httpGet.addHeader(header); } } // retrieve local java configured network in case there is the need to // authenticate a proxy ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner( httpClient.getConnectionManager().getSchemeRegistry(), ProxySelector.getDefault()); httpClient.setRoutePlanner(routePlanner); // Set preference order for authentication options. // In particular, we don't add AuthPolicy.SPNEGO, which is given preference over NTLM in // servers that support both, as it is more secure. However, we don't seem to handle it // very well, so we leave it off the list. // See http://hc.apache.org/httpcomponents-client-ga/tutorial/html/authentication.html for // more info. List<String> authpref = new ArrayList<String>(); authpref.add(AuthPolicy.BASIC); authpref.add(AuthPolicy.DIGEST); authpref.add(AuthPolicy.NTLM); httpClient.getParams().setParameter(AuthPNames.PROXY_AUTH_PREF, authpref); httpClient.getParams().setParameter(AuthPNames.TARGET_AUTH_PREF, authpref); if (DEBUG) { try { URI uri = new URI(url); ProxySelector sel = routePlanner.getProxySelector(); if (sel != null && uri.getScheme().startsWith("httP")) { //$NON-NLS-1$ List<Proxy> list = sel.select(uri); System.out.printf( "SdkLib.UrlOpener:\n Connect to: %s\n Proxy List: %s\n", //$NON-NLS-1$ url, list == null ? "(null)" : Arrays.toString(list.toArray()));//$NON-NLS-1$ } } catch (Exception e) { System.out.printf( "SdkLib.UrlOpener: Failed to get proxy info for %s: %s\n", //$NON-NLS-1$ url, e.toString()); } } boolean trying = true; // loop while the response is being fetched while (trying) { // connect and get status code HttpResponse response = httpClient.execute(httpGet, localContext); int statusCode = response.getStatusLine().getStatusCode(); if (DEBUG) { System.out.printf(" Status: %d\n", statusCode); //$NON-NLS-1$ } // check whether any authentication is required AuthState authenticationState = null; if (statusCode == HttpStatus.SC_UNAUTHORIZED) { // Target host authentication required authenticationState = (AuthState) localContext .getAttribute(ClientContext.TARGET_AUTH_STATE); } if (statusCode == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) { // Proxy authentication required authenticationState = (AuthState) localContext .getAttribute(ClientContext.PROXY_AUTH_STATE); } if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NOT_MODIFIED) { // in case the status is OK and there is a realm and result, // cache it if (realm != null && result != null) { sRealmCache.put(realm, result); } } // there is the need for authentication if (authenticationState != null) { // get scope and realm AuthScope authScope = authenticationState.getAuthScope(); // If the current realm is different from the last one it means // a pass was performed successfully to the last URL, therefore // cache the last realm if (realm != null && !realm.equals(authScope.getRealm())) { sRealmCache.put(realm, result); } realm = authScope.getRealm(); // in case there is cache for this Realm, use it to authenticate if (sRealmCache.containsKey(realm)) { result = sRealmCache.get(realm); } else { // since there is no cache, request for login and password result = monitor.displayLoginCredentialsPrompt("Site Authentication", "Please login to the following domain: " + realm + "\n\nServer requiring authentication:\n" + authScope.getHost()); if (result == null) { throw new CanceledByUserException("User canceled login dialog."); } } // retrieve authentication data String user = result.getUserName(); String password = result.getPassword(); String workstation = result.getWorkstation(); String domain = result.getDomain(); // proceed in case there is indeed a user if (user != null && !user.isEmpty()) { Credentials credentials = new NTCredentials(user, password, workstation, domain); httpClient.getCredentialsProvider().setCredentials(authScope, credentials); trying = true; } else { trying = false; } } else { trying = false; } HttpEntity entity = response.getEntity(); if (entity != null) { if (trying) { // in case another pass to the Http Client will be performed, close the entity. entity.getContent().close(); } else { // since no pass to the Http Client is needed, retrieve the // entity's content. // Note: don't use something like a BufferedHttpEntity since it would consume // all content and store it in memory, resulting in an OutOfMemory exception // on a large download. InputStream is = new FilterInputStream(entity.getContent()) { @Override public void close() throws IOException { // Since Http Client is no longer needed, close it. // Bug #21167: we need to tell http client to shutdown // first, otherwise the super.close() would continue // downloading and not return till complete. httpClient.getConnectionManager().shutdown(); super.close(); } }; HttpResponse outResponse = new BasicHttpResponse(response.getStatusLine()); outResponse.setHeaders(response.getAllHeaders()); outResponse.setLocale(response.getLocale()); return Pair.of(is, outResponse); } } else if (statusCode == HttpStatus.SC_NOT_MODIFIED) { // It's ok to not have an entity (e.g. nothing to download) for a 304 HttpResponse outResponse = new BasicHttpResponse(response.getStatusLine()); outResponse.setHeaders(response.getAllHeaders()); outResponse.setLocale(response.getLocale()); return Pair.of(null, outResponse); } } // We get here if we did not succeed. Callers do not expect a null result. httpClient.getConnectionManager().shutdown(); throw new FileNotFoundException(url); } }