mobile/android/base/sync/net/BaseResource.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 package org.mozilla.gecko.sync.net;
     7 import java.io.BufferedReader;
     8 import java.io.IOException;
     9 import java.io.UnsupportedEncodingException;
    10 import java.lang.ref.WeakReference;
    11 import java.net.URI;
    12 import java.net.URISyntaxException;
    13 import java.security.GeneralSecurityException;
    14 import java.security.KeyManagementException;
    15 import java.security.NoSuchAlgorithmException;
    16 import java.security.SecureRandom;
    18 import javax.net.ssl.SSLContext;
    20 import org.json.simple.JSONArray;
    21 import org.json.simple.JSONObject;
    22 import org.mozilla.gecko.background.common.log.Logger;
    23 import org.mozilla.gecko.sync.ExtendedJSONObject;
    25 import ch.boye.httpclientandroidlib.Header;
    26 import ch.boye.httpclientandroidlib.HttpEntity;
    27 import ch.boye.httpclientandroidlib.HttpResponse;
    28 import ch.boye.httpclientandroidlib.HttpVersion;
    29 import ch.boye.httpclientandroidlib.client.AuthCache;
    30 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
    31 import ch.boye.httpclientandroidlib.client.methods.HttpDelete;
    32 import ch.boye.httpclientandroidlib.client.methods.HttpGet;
    33 import ch.boye.httpclientandroidlib.client.methods.HttpPost;
    34 import ch.boye.httpclientandroidlib.client.methods.HttpPut;
    35 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
    36 import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
    37 import ch.boye.httpclientandroidlib.client.protocol.ClientContext;
    38 import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
    39 import ch.boye.httpclientandroidlib.conn.scheme.PlainSocketFactory;
    40 import ch.boye.httpclientandroidlib.conn.scheme.Scheme;
    41 import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
    42 import ch.boye.httpclientandroidlib.conn.ssl.SSLSocketFactory;
    43 import ch.boye.httpclientandroidlib.entity.StringEntity;
    44 import ch.boye.httpclientandroidlib.impl.client.BasicAuthCache;
    45 import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
    46 import ch.boye.httpclientandroidlib.impl.conn.tsccm.ThreadSafeClientConnManager;
    47 import ch.boye.httpclientandroidlib.params.HttpConnectionParams;
    48 import ch.boye.httpclientandroidlib.params.HttpParams;
    49 import ch.boye.httpclientandroidlib.params.HttpProtocolParams;
    50 import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
    51 import ch.boye.httpclientandroidlib.protocol.HttpContext;
    52 import ch.boye.httpclientandroidlib.util.EntityUtils;
    54 /**
    55  * Provide simple HTTP access to a Sync server or similar.
    56  * Implements Basic Auth by asking its delegate for credentials.
    57  * Communicates with a ResourceDelegate to asynchronously return responses and errors.
    58  * Exposes simple get/post/put/delete methods.
    59  */
    60 public class BaseResource implements Resource {
    61   private static final String ANDROID_LOOPBACK_IP = "10.0.2.2";
    63   private static final int MAX_TOTAL_CONNECTIONS     = 20;
    64   private static final int MAX_CONNECTIONS_PER_ROUTE = 10;
    66   private boolean retryOnFailedRequest = true;
    68   public static boolean rewriteLocalhost = true;
    70   private static final String LOG_TAG = "BaseResource";
    72   protected final URI uri;
    73   protected BasicHttpContext context;
    74   protected DefaultHttpClient client;
    75   public    ResourceDelegate delegate;
    76   protected HttpRequestBase request;
    77   public String charset = "utf-8";
    79   protected static WeakReference<HttpResponseObserver> httpResponseObserver = null;
    81   public BaseResource(String uri) throws URISyntaxException {
    82     this(uri, rewriteLocalhost);
    83   }
    85   public BaseResource(URI uri) {
    86     this(uri, rewriteLocalhost);
    87   }
    89   public BaseResource(String uri, boolean rewrite) throws URISyntaxException {
    90     this(new URI(uri), rewrite);
    91   }
    93   public BaseResource(URI uri, boolean rewrite) {
    94     if (uri == null) {
    95       throw new IllegalArgumentException("uri must not be null");
    96     }
    97     if (rewrite && "localhost".equals(uri.getHost())) {
    98       // Rewrite localhost URIs to refer to the special Android emulator loopback passthrough interface.
    99       Logger.debug(LOG_TAG, "Rewriting " + uri + " to point to " + ANDROID_LOOPBACK_IP + ".");
   100       try {
   101         this.uri = new URI(uri.getScheme(), uri.getUserInfo(), ANDROID_LOOPBACK_IP, uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
   102       } catch (URISyntaxException e) {
   103         Logger.error(LOG_TAG, "Got error rewriting URI for Android emulator.", e);
   104         throw new IllegalArgumentException("Invalid URI", e);
   105       }
   106     } else {
   107       this.uri = uri;
   108     }
   109   }
   111   public static synchronized HttpResponseObserver getHttpResponseObserver() {
   112     if (httpResponseObserver == null) {
   113       return null;
   114     }
   115     return httpResponseObserver.get();
   116   }
   118   public static synchronized void setHttpResponseObserver(HttpResponseObserver newHttpResponseObserver) {
   119     if (httpResponseObserver != null) {
   120       httpResponseObserver.clear();
   121     }
   122     httpResponseObserver = new WeakReference<HttpResponseObserver>(newHttpResponseObserver);
   123   }
   125   @Override
   126   public URI getURI() {
   127     return this.uri;
   128   }
   130   @Override
   131   public String getURIString() {
   132     return this.uri.toString();
   133   }
   135   @Override
   136   public String getHostname() {
   137     return this.getURI().getHost();
   138   }
   140   /**
   141    * This shuts up HttpClient, which will otherwise debug log about there
   142    * being no auth cache in the context.
   143    */
   144   private static void addAuthCacheToContext(HttpUriRequest request, HttpContext context) {
   145     AuthCache authCache = new BasicAuthCache();                // Not thread safe.
   146     context.setAttribute(ClientContext.AUTH_CACHE, authCache);
   147   }
   149   /**
   150    * Invoke this after delegate and request have been set.
   151    * @throws NoSuchAlgorithmException
   152    * @throws KeyManagementException
   153    */
   154   protected void prepareClient() throws KeyManagementException, NoSuchAlgorithmException, GeneralSecurityException {
   155     context = new BasicHttpContext();
   157     // We could reuse these client instances, except that we mess around
   158     // with their parameters… so we'd need a pool of some kind.
   159     client = new DefaultHttpClient(getConnectionManager());
   161     // TODO: Eventually we should use Apache HttpAsyncClient. It's not out of alpha yet.
   162     // Until then, we synchronously make the request, then invoke our delegate's callback.
   163     AuthHeaderProvider authHeaderProvider = delegate.getAuthHeaderProvider();
   164     if (authHeaderProvider != null) {
   165       Header authHeader = authHeaderProvider.getAuthHeader(request, context, client);
   166       if (authHeader != null) {
   167         request.addHeader(authHeader);
   168         Logger.debug(LOG_TAG, "Added auth header.");
   169       }
   170     }
   172     addAuthCacheToContext(request, context);
   174     HttpParams params = client.getParams();
   175     HttpConnectionParams.setConnectionTimeout(params, delegate.connectionTimeout());
   176     HttpConnectionParams.setSoTimeout(params, delegate.socketTimeout());
   177     HttpConnectionParams.setStaleCheckingEnabled(params, false);
   178     HttpProtocolParams.setContentCharset(params, charset);
   179     HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
   180     final String userAgent = delegate.getUserAgent();
   181     if (userAgent != null) {
   182       HttpProtocolParams.setUserAgent(params, userAgent);
   183     }
   184     delegate.addHeaders(request, client);
   185   }
   187   private static Object connManagerMonitor = new Object();
   188   private static ClientConnectionManager connManager;
   190   // Call within a synchronized block on connManagerMonitor.
   191   private static ClientConnectionManager enableTLSConnectionManager() throws KeyManagementException, NoSuchAlgorithmException  {
   192     SSLContext sslContext = SSLContext.getInstance("TLS");
   193     sslContext.init(null, null, new SecureRandom());
   194     SSLSocketFactory sf = new TLSSocketFactory(sslContext);
   195     SchemeRegistry schemeRegistry = new SchemeRegistry();
   196     schemeRegistry.register(new Scheme("https", 443, sf));
   197     schemeRegistry.register(new Scheme("http", 80, new PlainSocketFactory()));
   198     ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(schemeRegistry);
   200     cm.setMaxTotal(MAX_TOTAL_CONNECTIONS);
   201     cm.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE);
   202     connManager = cm;
   203     return cm;
   204   }
   206   public static ClientConnectionManager getConnectionManager() throws KeyManagementException, NoSuchAlgorithmException
   207                                                          {
   208     // TODO: shutdown.
   209     synchronized (connManagerMonitor) {
   210       if (connManager != null) {
   211         return connManager;
   212       }
   213       return enableTLSConnectionManager();
   214     }
   215   }
   217   /**
   218    * Do some cleanup, so we don't need the stale connection check.
   219    */
   220   public static void closeExpiredConnections() {
   221     ClientConnectionManager connectionManager;
   222     synchronized (connManagerMonitor) {
   223       connectionManager = connManager;
   224     }
   225     if (connectionManager == null) {
   226       return;
   227     }
   228     Logger.trace(LOG_TAG, "Closing expired connections.");
   229     connectionManager.closeExpiredConnections();
   230   }
   232   public static void shutdownConnectionManager() {
   233     ClientConnectionManager connectionManager;
   234     synchronized (connManagerMonitor) {
   235       connectionManager = connManager;
   236       connManager = null;
   237     }
   238     if (connectionManager == null) {
   239       return;
   240     }
   241     Logger.debug(LOG_TAG, "Shutting down connection manager.");
   242     connectionManager.shutdown();
   243   }
   245   private void execute() {
   246     HttpResponse response;
   247     try {
   248       response = client.execute(request, context);
   249       Logger.debug(LOG_TAG, "Response: " + response.getStatusLine().toString());
   250     } catch (ClientProtocolException e) {
   251       delegate.handleHttpProtocolException(e);
   252       return;
   253     } catch (IOException e) {
   254       Logger.debug(LOG_TAG, "I/O exception returned from execute.");
   255       if (!retryOnFailedRequest) {
   256         delegate.handleHttpIOException(e);
   257       } else {
   258         retryRequest();
   259       }
   260       return;
   261     } catch (Exception e) {
   262       // Bug 740731: Don't let an exception fall through. Wrapping isn't
   263       // optimal, but often the exception is treated as an Exception anyway.
   264       if (!retryOnFailedRequest) {
   265         // Bug 769671: IOException(Throwable cause) was added only in API level 9.
   266         final IOException ex = new IOException();
   267         ex.initCause(e);
   268         delegate.handleHttpIOException(ex);
   269       } else {
   270         retryRequest();
   271       }
   272       return;
   273     }
   275     // Don't retry if the observer or delegate throws!
   276     HttpResponseObserver observer = getHttpResponseObserver();
   277     if (observer != null) {
   278       observer.observeHttpResponse(response);
   279     }
   280     delegate.handleHttpResponse(response);
   281   }
   283   private void retryRequest() {
   284     // Only retry once.
   285     retryOnFailedRequest = false;
   286     Logger.debug(LOG_TAG, "Retrying request...");
   287     this.execute();
   288   }
   290   private void go(HttpRequestBase request) {
   291    if (delegate == null) {
   292       throw new IllegalArgumentException("No delegate provided.");
   293     }
   294     this.request = request;
   295     try {
   296       this.prepareClient();
   297     } catch (KeyManagementException e) {
   298       Logger.error(LOG_TAG, "Couldn't prepare client.", e);
   299       delegate.handleTransportException(e);
   300       return;
   301     } catch (NoSuchAlgorithmException e) {
   302       Logger.error(LOG_TAG, "Couldn't prepare client.", e);
   303       delegate.handleTransportException(e);
   304       return;
   305     } catch (GeneralSecurityException e) {
   306       Logger.error(LOG_TAG, "Couldn't prepare client.", e);
   307       delegate.handleTransportException(e);
   308       return;
   309     } catch (Exception e) {
   310       // Bug 740731: Don't let an exception fall through. Wrapping isn't
   311       // optimal, but often the exception is treated as an Exception anyway.
   312       delegate.handleTransportException(new GeneralSecurityException(e));
   313       return;
   314     }
   315     this.execute();
   316   }
   318   @Override
   319   public void get() {
   320     Logger.debug(LOG_TAG, "HTTP GET " + this.uri.toASCIIString());
   321     this.go(new HttpGet(this.uri));
   322   }
   324   /**
   325    * Perform an HTTP GET as with {@link BaseResource#get()}, returning only
   326    * after callbacks have been invoked.
   327    */
   328   public void getBlocking() {
   329     // Until we use the asynchronous Apache HttpClient, we can simply call
   330     // through.
   331     this.get();
   332   }
   334   @Override
   335   public void delete() {
   336     Logger.debug(LOG_TAG, "HTTP DELETE " + this.uri.toASCIIString());
   337     this.go(new HttpDelete(this.uri));
   338   }
   340   @Override
   341   public void post(HttpEntity body) {
   342     Logger.debug(LOG_TAG, "HTTP POST " + this.uri.toASCIIString());
   343     HttpPost request = new HttpPost(this.uri);
   344     request.setEntity(body);
   345     this.go(request);
   346   }
   348   @Override
   349   public void put(HttpEntity body) {
   350     Logger.debug(LOG_TAG, "HTTP PUT " + this.uri.toASCIIString());
   351     HttpPut request = new HttpPut(this.uri);
   352     request.setEntity(body);
   353     this.go(request);
   354   }
   356   protected static StringEntity stringEntityWithContentTypeApplicationJSON(String s) throws UnsupportedEncodingException {
   357     StringEntity e = new StringEntity(s, "UTF-8");
   358     e.setContentType("application/json");
   359     return e;
   360   }
   362   /**
   363    * Helper for turning a JSON object into a payload.
   364    * @throws UnsupportedEncodingException
   365    */
   366   protected static StringEntity jsonEntity(JSONObject body) throws UnsupportedEncodingException {
   367     return stringEntityWithContentTypeApplicationJSON(body.toJSONString());
   368   }
   370   /**
   371    * Helper for turning an extended JSON object into a payload.
   372    * @throws UnsupportedEncodingException
   373    */
   374   protected static StringEntity jsonEntity(ExtendedJSONObject body) throws UnsupportedEncodingException {
   375     return stringEntityWithContentTypeApplicationJSON(body.toJSONString());
   376   }
   378   /**
   379    * Helper for turning a JSON array into a payload.
   380    * @throws UnsupportedEncodingException
   381    */
   382   protected static HttpEntity jsonEntity(JSONArray toPOST) throws UnsupportedEncodingException {
   383     return stringEntityWithContentTypeApplicationJSON(toPOST.toJSONString());
   384   }
   386   /**
   387    * Best-effort attempt to ensure that the entity has been fully consumed and
   388    * that the underlying stream has been closed.
   389    *
   390    * This releases the connection back to the connection pool.
   391    *
   392    * @param entity The HttpEntity to be consumed.
   393    */
   394   public static void consumeEntity(HttpEntity entity) {
   395     try {
   396       EntityUtils.consume(entity);
   397     } catch (IOException e) {
   398       // Doesn't matter.
   399     }
   400   }
   402   /**
   403    * Best-effort attempt to ensure that the entity corresponding to the given
   404    * HTTP response has been fully consumed and that the underlying stream has
   405    * been closed.
   406    *
   407    * This releases the connection back to the connection pool.
   408    *
   409    * @param response
   410    *          The HttpResponse to be consumed.
   411    */
   412   public static void consumeEntity(HttpResponse response) {
   413     if (response == null) {
   414       return;
   415     }
   416     try {
   417       EntityUtils.consume(response.getEntity());
   418     } catch (IOException e) {
   419     }
   420   }
   422   /**
   423    * Best-effort attempt to ensure that the entity corresponding to the given
   424    * Sync storage response has been fully consumed and that the underlying
   425    * stream has been closed.
   426    *
   427    * This releases the connection back to the connection pool.
   428    *
   429    * @param response
   430    *          The SyncStorageResponse to be consumed.
   431    */
   432   public static void consumeEntity(SyncStorageResponse response) {
   433     if (response.httpResponse() == null) {
   434       return;
   435     }
   436     consumeEntity(response.httpResponse());
   437   }
   439   /**
   440    * Best-effort attempt to ensure that the reader has been fully consumed, so
   441    * that the underlying stream will be closed.
   442    *
   443    * This should allow the connection to be released back to the connection pool.
   444    *
   445    * @param reader The BufferedReader to be consumed.
   446    */
   447   public static void consumeReader(BufferedReader reader) {
   448     try {
   449       reader.close();
   450     } catch (IOException e) {
   451       // Do nothing.
   452     }
   453   }
   455   public void post(JSONArray jsonArray) throws UnsupportedEncodingException {
   456     post(jsonEntity(jsonArray));
   457   }
   459   public void put(JSONObject jsonObject) throws UnsupportedEncodingException {
   460     put(jsonEntity(jsonObject));
   461   }
   463   public void post(ExtendedJSONObject o) throws UnsupportedEncodingException {
   464     post(jsonEntity(o));
   465   }
   467   public void post(JSONObject jsonObject) throws UnsupportedEncodingException {
   468     post(jsonEntity(jsonObject));
   469   }
   470 }

mercurial