Wed, 31 Dec 2014 06:09:35 +0100
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 }