mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
415 lines
14 KiB
Java
415 lines
14 KiB
Java
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
package org.mozilla.gecko.sync.net;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.IOException;
|
|
import java.lang.ref.WeakReference;
|
|
import java.net.URI;
|
|
import java.net.URISyntaxException;
|
|
import java.security.GeneralSecurityException;
|
|
import java.security.KeyManagementException;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.security.SecureRandom;
|
|
|
|
import javax.net.ssl.SSLContext;
|
|
|
|
import org.mozilla.gecko.sync.Logger;
|
|
|
|
import ch.boye.httpclientandroidlib.Header;
|
|
import ch.boye.httpclientandroidlib.HttpEntity;
|
|
import ch.boye.httpclientandroidlib.HttpResponse;
|
|
import ch.boye.httpclientandroidlib.HttpVersion;
|
|
import ch.boye.httpclientandroidlib.auth.Credentials;
|
|
import ch.boye.httpclientandroidlib.auth.UsernamePasswordCredentials;
|
|
import ch.boye.httpclientandroidlib.client.AuthCache;
|
|
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
|
|
import ch.boye.httpclientandroidlib.client.methods.HttpDelete;
|
|
import ch.boye.httpclientandroidlib.client.methods.HttpGet;
|
|
import ch.boye.httpclientandroidlib.client.methods.HttpPost;
|
|
import ch.boye.httpclientandroidlib.client.methods.HttpPut;
|
|
import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
|
|
import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
|
|
import ch.boye.httpclientandroidlib.client.protocol.ClientContext;
|
|
import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
|
|
import ch.boye.httpclientandroidlib.conn.scheme.PlainSocketFactory;
|
|
import ch.boye.httpclientandroidlib.conn.scheme.Scheme;
|
|
import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
|
|
import ch.boye.httpclientandroidlib.conn.ssl.SSLSocketFactory;
|
|
import ch.boye.httpclientandroidlib.impl.auth.BasicScheme;
|
|
import ch.boye.httpclientandroidlib.impl.client.BasicAuthCache;
|
|
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
|
|
import ch.boye.httpclientandroidlib.impl.conn.tsccm.ThreadSafeClientConnManager;
|
|
import ch.boye.httpclientandroidlib.params.HttpConnectionParams;
|
|
import ch.boye.httpclientandroidlib.params.HttpParams;
|
|
import ch.boye.httpclientandroidlib.params.HttpProtocolParams;
|
|
import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
|
|
import ch.boye.httpclientandroidlib.protocol.HttpContext;
|
|
import ch.boye.httpclientandroidlib.util.EntityUtils;
|
|
|
|
/**
|
|
* Provide simple HTTP access to a Sync server or similar.
|
|
* Implements Basic Auth by asking its delegate for credentials.
|
|
* Communicates with a ResourceDelegate to asynchronously return responses and errors.
|
|
* Exposes simple get/post/put/delete methods.
|
|
*/
|
|
public class BaseResource implements Resource {
|
|
private static final String ANDROID_LOOPBACK_IP = "10.0.2.2";
|
|
|
|
private static final int MAX_TOTAL_CONNECTIONS = 20;
|
|
private static final int MAX_CONNECTIONS_PER_ROUTE = 10;
|
|
|
|
private boolean retryOnFailedRequest = true;
|
|
|
|
public static boolean rewriteLocalhost = true;
|
|
|
|
private static final String LOG_TAG = "BaseResource";
|
|
|
|
protected URI uri;
|
|
protected BasicHttpContext context;
|
|
protected DefaultHttpClient client;
|
|
public ResourceDelegate delegate;
|
|
protected HttpRequestBase request;
|
|
public String charset = "utf-8";
|
|
|
|
protected static WeakReference<HttpResponseObserver> httpResponseObserver = null;
|
|
|
|
public BaseResource(String uri) throws URISyntaxException {
|
|
this(uri, rewriteLocalhost);
|
|
}
|
|
|
|
public BaseResource(URI uri) {
|
|
this(uri, rewriteLocalhost);
|
|
}
|
|
|
|
public BaseResource(String uri, boolean rewrite) throws URISyntaxException {
|
|
this(new URI(uri), rewrite);
|
|
}
|
|
|
|
public BaseResource(URI uri, boolean rewrite) {
|
|
if (rewrite && uri.getHost().equals("localhost")) {
|
|
// Rewrite localhost URIs to refer to the special Android emulator loopback passthrough interface.
|
|
Logger.debug(LOG_TAG, "Rewriting " + uri + " to point to " + ANDROID_LOOPBACK_IP + ".");
|
|
try {
|
|
this.uri = new URI(uri.getScheme(), uri.getUserInfo(), ANDROID_LOOPBACK_IP, uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
|
|
} catch (URISyntaxException e) {
|
|
Logger.error(LOG_TAG, "Got error rewriting URI for Android emulator.", e);
|
|
}
|
|
} else {
|
|
this.uri = uri;
|
|
}
|
|
}
|
|
|
|
public static synchronized HttpResponseObserver getHttpResponseObserver() {
|
|
if (httpResponseObserver == null) {
|
|
return null;
|
|
}
|
|
return httpResponseObserver.get();
|
|
}
|
|
|
|
public static synchronized void setHttpResponseObserver(HttpResponseObserver newHttpResponseObserver) {
|
|
if (httpResponseObserver != null) {
|
|
httpResponseObserver.clear();
|
|
}
|
|
httpResponseObserver = new WeakReference<HttpResponseObserver>(newHttpResponseObserver);
|
|
}
|
|
|
|
public URI getURI() {
|
|
return this.uri;
|
|
}
|
|
|
|
/**
|
|
* This shuts up HttpClient, which will otherwise debug log about there
|
|
* being no auth cache in the context.
|
|
*/
|
|
private static void addAuthCacheToContext(HttpUriRequest request, HttpContext context) {
|
|
AuthCache authCache = new BasicAuthCache(); // Not thread safe.
|
|
context.setAttribute(ClientContext.AUTH_CACHE, authCache);
|
|
}
|
|
|
|
/**
|
|
* Return a Header object representing an Authentication header for HTTP Basic.
|
|
*/
|
|
public static Header getBasicAuthHeader(final String credentials) {
|
|
Credentials creds = new UsernamePasswordCredentials(credentials);
|
|
|
|
// This must be UTF-8 to generate the same Basic Auth headers as desktop for non-ASCII passwords.
|
|
return BasicScheme.authenticate(creds, "UTF-8", false);
|
|
}
|
|
|
|
/**
|
|
* Apply the provided credentials string to the provided request.
|
|
* @param credentials a string, "user:pass".
|
|
*/
|
|
private static void applyCredentials(String credentials, HttpUriRequest request, HttpContext context) {
|
|
request.addHeader(getBasicAuthHeader(credentials));
|
|
Logger.trace(LOG_TAG, "Adding Basic Auth header.");
|
|
}
|
|
|
|
/**
|
|
* Invoke this after delegate and request have been set.
|
|
* @throws NoSuchAlgorithmException
|
|
* @throws KeyManagementException
|
|
*/
|
|
private void prepareClient() throws KeyManagementException, NoSuchAlgorithmException {
|
|
context = new BasicHttpContext();
|
|
|
|
// We could reuse these client instances, except that we mess around
|
|
// with their parameters… so we'd need a pool of some kind.
|
|
client = new DefaultHttpClient(getConnectionManager());
|
|
|
|
// TODO: Eventually we should use Apache HttpAsyncClient. It's not out of alpha yet.
|
|
// Until then, we synchronously make the request, then invoke our delegate's callback.
|
|
String credentials = delegate.getCredentials();
|
|
if (credentials != null) {
|
|
BaseResource.applyCredentials(credentials, request, context);
|
|
}
|
|
|
|
addAuthCacheToContext(request, context);
|
|
|
|
HttpParams params = client.getParams();
|
|
HttpConnectionParams.setConnectionTimeout(params, delegate.connectionTimeout());
|
|
HttpConnectionParams.setSoTimeout(params, delegate.socketTimeout());
|
|
HttpConnectionParams.setStaleCheckingEnabled(params, false);
|
|
HttpProtocolParams.setContentCharset(params, charset);
|
|
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
|
|
delegate.addHeaders(request, client);
|
|
}
|
|
|
|
private static Object connManagerMonitor = new Object();
|
|
private static ClientConnectionManager connManager;
|
|
|
|
// Call within a synchronized block on connManagerMonitor.
|
|
private static ClientConnectionManager enableTLSConnectionManager() throws KeyManagementException, NoSuchAlgorithmException {
|
|
SSLContext sslContext = SSLContext.getInstance("TLS");
|
|
sslContext.init(null, null, new SecureRandom());
|
|
SSLSocketFactory sf = new TLSSocketFactory(sslContext);
|
|
SchemeRegistry schemeRegistry = new SchemeRegistry();
|
|
schemeRegistry.register(new Scheme("https", 443, sf));
|
|
schemeRegistry.register(new Scheme("http", 80, new PlainSocketFactory()));
|
|
ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(schemeRegistry);
|
|
|
|
cm.setMaxTotal(MAX_TOTAL_CONNECTIONS);
|
|
cm.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE);
|
|
connManager = cm;
|
|
return cm;
|
|
}
|
|
|
|
public static ClientConnectionManager getConnectionManager() throws KeyManagementException, NoSuchAlgorithmException
|
|
{
|
|
// TODO: shutdown.
|
|
synchronized (connManagerMonitor) {
|
|
if (connManager != null) {
|
|
return connManager;
|
|
}
|
|
return enableTLSConnectionManager();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Do some cleanup, so we don't need the stale connection check.
|
|
*/
|
|
public static void closeExpiredConnections() {
|
|
ClientConnectionManager connectionManager;
|
|
synchronized (connManagerMonitor) {
|
|
connectionManager = connManager;
|
|
}
|
|
if (connectionManager == null) {
|
|
return;
|
|
}
|
|
Logger.trace(LOG_TAG, "Closing expired connections.");
|
|
connectionManager.closeExpiredConnections();
|
|
}
|
|
|
|
public static void shutdownConnectionManager() {
|
|
ClientConnectionManager connectionManager;
|
|
synchronized (connManagerMonitor) {
|
|
connectionManager = connManager;
|
|
connManager = null;
|
|
}
|
|
if (connectionManager == null) {
|
|
return;
|
|
}
|
|
Logger.debug(LOG_TAG, "Shutting down connection manager.");
|
|
connectionManager.shutdown();
|
|
}
|
|
|
|
private void execute() {
|
|
HttpResponse response;
|
|
try {
|
|
response = client.execute(request, context);
|
|
Logger.debug(LOG_TAG, "Response: " + response.getStatusLine().toString());
|
|
} catch (ClientProtocolException e) {
|
|
delegate.handleHttpProtocolException(e);
|
|
return;
|
|
} catch (IOException e) {
|
|
Logger.debug(LOG_TAG, "I/O exception returned from execute.");
|
|
if (!retryOnFailedRequest) {
|
|
delegate.handleHttpIOException(e);
|
|
} else {
|
|
retryRequest();
|
|
}
|
|
return;
|
|
} catch (Exception e) {
|
|
// Bug 740731: Don't let an exception fall through. Wrapping isn't
|
|
// optimal, but often the exception is treated as an Exception anyway.
|
|
if (!retryOnFailedRequest) {
|
|
// Bug 769671: IOException(Throwable cause) was added only in API level 9.
|
|
final IOException ex = new IOException();
|
|
ex.initCause(e);
|
|
delegate.handleHttpIOException(ex);
|
|
} else {
|
|
retryRequest();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Don't retry if the observer or delegate throws!
|
|
HttpResponseObserver observer = getHttpResponseObserver();
|
|
if (observer != null) {
|
|
observer.observeHttpResponse(response);
|
|
}
|
|
delegate.handleHttpResponse(response);
|
|
}
|
|
|
|
private void retryRequest() {
|
|
// Only retry once.
|
|
retryOnFailedRequest = false;
|
|
Logger.debug(LOG_TAG, "Retrying request...");
|
|
this.execute();
|
|
}
|
|
|
|
private void go(HttpRequestBase request) {
|
|
if (delegate == null) {
|
|
throw new IllegalArgumentException("No delegate provided.");
|
|
}
|
|
this.request = request;
|
|
try {
|
|
this.prepareClient();
|
|
} catch (KeyManagementException e) {
|
|
Logger.error(LOG_TAG, "Couldn't prepare client.", e);
|
|
delegate.handleTransportException(e);
|
|
return;
|
|
} catch (NoSuchAlgorithmException e) {
|
|
Logger.error(LOG_TAG, "Couldn't prepare client.", e);
|
|
delegate.handleTransportException(e);
|
|
return;
|
|
} catch (Exception e) {
|
|
// Bug 740731: Don't let an exception fall through. Wrapping isn't
|
|
// optimal, but often the exception is treated as an Exception anyway.
|
|
delegate.handleTransportException(new GeneralSecurityException(e));
|
|
return;
|
|
}
|
|
this.execute();
|
|
}
|
|
|
|
@Override
|
|
public void get() {
|
|
Logger.debug(LOG_TAG, "HTTP GET " + this.uri.toASCIIString());
|
|
this.go(new HttpGet(this.uri));
|
|
}
|
|
|
|
/**
|
|
* Perform an HTTP GET as with {@link BaseResource#get()}, returning only
|
|
* after callbacks have been invoked.
|
|
*/
|
|
public void getBlocking() {
|
|
// Until we use the asynchronous Apache HttpClient, we can simply call
|
|
// through.
|
|
this.get();
|
|
}
|
|
|
|
@Override
|
|
public void delete() {
|
|
Logger.debug(LOG_TAG, "HTTP DELETE " + this.uri.toASCIIString());
|
|
this.go(new HttpDelete(this.uri));
|
|
}
|
|
|
|
@Override
|
|
public void post(HttpEntity body) {
|
|
Logger.debug(LOG_TAG, "HTTP POST " + this.uri.toASCIIString());
|
|
HttpPost request = new HttpPost(this.uri);
|
|
request.setEntity(body);
|
|
this.go(request);
|
|
}
|
|
|
|
@Override
|
|
public void put(HttpEntity body) {
|
|
Logger.debug(LOG_TAG, "HTTP PUT " + this.uri.toASCIIString());
|
|
HttpPut request = new HttpPut(this.uri);
|
|
request.setEntity(body);
|
|
this.go(request);
|
|
}
|
|
|
|
/**
|
|
* Best-effort attempt to ensure that the entity has been fully consumed and
|
|
* that the underlying stream has been closed.
|
|
*
|
|
* This releases the connection back to the connection pool.
|
|
*
|
|
* @param entity The HttpEntity to be consumed.
|
|
*/
|
|
public static void consumeEntity(HttpEntity entity) {
|
|
try {
|
|
EntityUtils.consume(entity);
|
|
} catch (IOException e) {
|
|
// Doesn't matter.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Best-effort attempt to ensure that the entity corresponding to the given
|
|
* HTTP response has been fully consumed and that the underlying stream has
|
|
* been closed.
|
|
*
|
|
* This releases the connection back to the connection pool.
|
|
*
|
|
* @param response
|
|
* The HttpResponse to be consumed.
|
|
*/
|
|
public static void consumeEntity(HttpResponse response) {
|
|
if (response == null) {
|
|
return;
|
|
}
|
|
try {
|
|
EntityUtils.consume(response.getEntity());
|
|
} catch (IOException e) {
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Best-effort attempt to ensure that the entity corresponding to the given
|
|
* Sync storage response has been fully consumed and that the underlying
|
|
* stream has been closed.
|
|
*
|
|
* This releases the connection back to the connection pool.
|
|
*
|
|
* @param response
|
|
* The SyncStorageResponse to be consumed.
|
|
*/
|
|
public static void consumeEntity(SyncStorageResponse response) {
|
|
if (response.httpResponse() == null) {
|
|
return;
|
|
}
|
|
consumeEntity(response.httpResponse());
|
|
}
|
|
|
|
/**
|
|
* Best-effort attempt to ensure that the reader has been fully consumed, so
|
|
* that the underlying stream will be closed.
|
|
*
|
|
* This should allow the connection to be released back to the connection pool.
|
|
*
|
|
* @param reader The BufferedReader to be consumed.
|
|
*/
|
|
public static void consumeReader(BufferedReader reader) {
|
|
try {
|
|
reader.close();
|
|
} catch (IOException e) {
|
|
// Do nothing.
|
|
}
|
|
}
|
|
}
|