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.

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

mercurial