| /* |
| * 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); |
| } |
| } |