blob: 338463cbbdbf9401047813cb71c2a92872263a19 [file] [log] [blame]
/*
* 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);
}
}