• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.net.http;
18 
19 import org.apache.http.Header;
20 import org.apache.http.HttpEntity;
21 import org.apache.http.HttpEntityEnclosingRequest;
22 import org.apache.http.HttpException;
23 import org.apache.http.HttpHost;
24 import org.apache.http.HttpRequest;
25 import org.apache.http.HttpRequestInterceptor;
26 import org.apache.http.HttpResponse;
27 import org.apache.http.entity.AbstractHttpEntity;
28 import org.apache.http.entity.ByteArrayEntity;
29 import org.apache.http.client.HttpClient;
30 import org.apache.http.client.ResponseHandler;
31 import org.apache.http.client.ClientProtocolException;
32 import org.apache.http.client.protocol.ClientContext;
33 import org.apache.http.client.methods.HttpUriRequest;
34 import org.apache.http.client.params.HttpClientParams;
35 import org.apache.http.conn.ClientConnectionManager;
36 import org.apache.http.conn.scheme.PlainSocketFactory;
37 import org.apache.http.conn.scheme.Scheme;
38 import org.apache.http.conn.scheme.SchemeRegistry;
39 import org.apache.http.conn.ssl.SSLSocketFactory;
40 import org.apache.http.impl.client.DefaultHttpClient;
41 import org.apache.http.impl.client.RequestWrapper;
42 import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
43 import org.apache.http.params.BasicHttpParams;
44 import org.apache.http.params.HttpConnectionParams;
45 import org.apache.http.params.HttpParams;
46 import org.apache.http.params.HttpProtocolParams;
47 import org.apache.http.protocol.BasicHttpProcessor;
48 import org.apache.http.protocol.HttpContext;
49 import org.apache.http.protocol.BasicHttpContext;
50 import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache;
51 import org.apache.harmony.xnet.provider.jsse.SSLContextImpl;
52 
53 import java.io.IOException;
54 import java.io.InputStream;
55 import java.io.ByteArrayOutputStream;
56 import java.io.OutputStream;
57 import java.util.zip.GZIPInputStream;
58 import java.util.zip.GZIPOutputStream;
59 import java.net.URI;
60 import java.security.KeyManagementException;
61 
62 import android.util.Log;
63 import android.content.ContentResolver;
64 import android.provider.Settings;
65 import android.text.TextUtils;
66 import android.os.SystemProperties;
67 
68 /**
69  * Subclass of the Apache {@link DefaultHttpClient} that is configured with
70  * reasonable default settings and registered schemes for Android, and
71  * also lets the user add {@link HttpRequestInterceptor} classes.
72  * Don't create this directly, use the {@link #newInstance} factory method.
73  *
74  * <p>This client processes cookies but does not retain them by default.
75  * To retain cookies, simply add a cookie store to the HttpContext:</p>
76  *
77  * <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre>
78  *
79  * {@hide}
80  */
81 public final class AndroidHttpClient implements HttpClient {
82 
83     // Gzip of data shorter than this probably won't be worthwhile
84     public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256;
85 
86     private static final String TAG = "AndroidHttpClient";
87 
88 
89     /** Set if HTTP requests are blocked from being executed on this thread */
90     private static final ThreadLocal<Boolean> sThreadBlocked =
91             new ThreadLocal<Boolean>();
92 
93     /** Interceptor throws an exception if the executing thread is blocked */
94     private static final HttpRequestInterceptor sThreadCheckInterceptor =
95             new HttpRequestInterceptor() {
96         public void process(HttpRequest request, HttpContext context) {
97             if (sThreadBlocked.get() != null && sThreadBlocked.get()) {
98                 throw new RuntimeException("This thread forbids HTTP requests");
99             }
100         }
101     };
102 
103     /**
104      * Create a new HttpClient with reasonable defaults (which you can update).
105      *
106      * @param userAgent to report in your HTTP requests.
107      * @param sessionCache persistent session cache
108      * @return AndroidHttpClient for you to use for all your requests.
109      */
newInstance(String userAgent, SSLClientSessionCache sessionCache)110     public static AndroidHttpClient newInstance(String userAgent,
111             SSLClientSessionCache sessionCache) {
112         HttpParams params = new BasicHttpParams();
113 
114         // Turn off stale checking.  Our connections break all the time anyway,
115         // and it's not worth it to pay the penalty of checking every time.
116         HttpConnectionParams.setStaleCheckingEnabled(params, false);
117 
118         // Default connection and socket timeout of 20 seconds.  Tweak to taste.
119         HttpConnectionParams.setConnectionTimeout(params, 20 * 1000);
120         HttpConnectionParams.setSoTimeout(params, 20 * 1000);
121         HttpConnectionParams.setSocketBufferSize(params, 8192);
122 
123         // Don't handle redirects -- return them to the caller.  Our code
124         // often wants to re-POST after a redirect, which we must do ourselves.
125         HttpClientParams.setRedirecting(params, false);
126 
127         // Set the specified user agent and register standard protocols.
128         HttpProtocolParams.setUserAgent(params, userAgent);
129         SchemeRegistry schemeRegistry = new SchemeRegistry();
130         schemeRegistry.register(new Scheme("http",
131                 PlainSocketFactory.getSocketFactory(), 80));
132         schemeRegistry.register(new Scheme("https",
133                 socketFactoryWithCache(sessionCache), 443));
134 
135         ClientConnectionManager manager =
136                 new ThreadSafeClientConnManager(params, schemeRegistry);
137 
138         // We use a factory method to modify superclass initialization
139         // parameters without the funny call-a-static-method dance.
140         return new AndroidHttpClient(manager, params);
141     }
142 
143     /**
144      * Returns a socket factory backed by the given persistent session cache.
145      *
146      * @param sessionCache to retrieve sessions from, null for no cache
147      */
socketFactoryWithCache( SSLClientSessionCache sessionCache)148     private static SSLSocketFactory socketFactoryWithCache(
149             SSLClientSessionCache sessionCache) {
150         if (sessionCache == null) {
151             // Use the default factory which doesn't support persistent
152             // caching.
153             return SSLSocketFactory.getSocketFactory();
154         }
155 
156         // Create a new SSL context backed by the cache.
157         // TODO: Keep a weak *identity* hash map of caches to engines. In the
158         // mean time, if we have two engines for the same cache, they'll still
159         // share sessions but will have to do so through the persistent cache.
160         SSLContextImpl sslContext = new SSLContextImpl();
161         try {
162             sslContext.engineInit(null, null, null, sessionCache, null);
163         } catch (KeyManagementException e) {
164             throw new AssertionError(e);
165         }
166         return new SSLSocketFactory(sslContext.engineGetSocketFactory());
167     }
168 
169     /**
170      * Create a new HttpClient with reasonable defaults (which you can update).
171      * @param userAgent to report in your HTTP requests.
172      * @return AndroidHttpClient for you to use for all your requests.
173      */
newInstance(String userAgent)174     public static AndroidHttpClient newInstance(String userAgent) {
175         return newInstance(userAgent, null /* session cache */);
176     }
177 
178     private final HttpClient delegate;
179 
180     private RuntimeException mLeakedException = new IllegalStateException(
181             "AndroidHttpClient created and never closed");
182 
AndroidHttpClient(ClientConnectionManager ccm, HttpParams params)183     private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) {
184         this.delegate = new DefaultHttpClient(ccm, params) {
185             @Override
186             protected BasicHttpProcessor createHttpProcessor() {
187                 // Add interceptor to prevent making requests from main thread.
188                 BasicHttpProcessor processor = super.createHttpProcessor();
189                 processor.addRequestInterceptor(sThreadCheckInterceptor);
190                 processor.addRequestInterceptor(new CurlLogger());
191 
192                 return processor;
193             }
194 
195             @Override
196             protected HttpContext createHttpContext() {
197                 // Same as DefaultHttpClient.createHttpContext() minus the
198                 // cookie store.
199                 HttpContext context = new BasicHttpContext();
200                 context.setAttribute(
201                         ClientContext.AUTHSCHEME_REGISTRY,
202                         getAuthSchemes());
203                 context.setAttribute(
204                         ClientContext.COOKIESPEC_REGISTRY,
205                         getCookieSpecs());
206                 context.setAttribute(
207                         ClientContext.CREDS_PROVIDER,
208                         getCredentialsProvider());
209                 return context;
210             }
211         };
212     }
213 
214     @Override
finalize()215     protected void finalize() throws Throwable {
216         super.finalize();
217         if (mLeakedException != null) {
218             Log.e(TAG, "Leak found", mLeakedException);
219             mLeakedException = null;
220         }
221     }
222 
223     /**
224      * Block this thread from executing HTTP requests.
225      * Used to guard against HTTP requests blocking the main application thread.
226      * @param blocked if HTTP requests run on this thread should be denied
227      */
setThreadBlocked(boolean blocked)228     public static void setThreadBlocked(boolean blocked) {
229         sThreadBlocked.set(blocked);
230     }
231 
232     /**
233      * Modifies a request to indicate to the server that we would like a
234      * gzipped response.  (Uses the "Accept-Encoding" HTTP header.)
235      * @param request the request to modify
236      * @see #getUngzippedContent
237      */
modifyRequestToAcceptGzipResponse(HttpRequest request)238     public static void modifyRequestToAcceptGzipResponse(HttpRequest request) {
239         request.addHeader("Accept-Encoding", "gzip");
240     }
241 
242     /**
243      * Gets the input stream from a response entity.  If the entity is gzipped
244      * then this will get a stream over the uncompressed data.
245      *
246      * @param entity the entity whose content should be read
247      * @return the input stream to read from
248      * @throws IOException
249      */
getUngzippedContent(HttpEntity entity)250     public static InputStream getUngzippedContent(HttpEntity entity)
251             throws IOException {
252         InputStream responseStream = entity.getContent();
253         if (responseStream == null) return responseStream;
254         Header header = entity.getContentEncoding();
255         if (header == null) return responseStream;
256         String contentEncoding = header.getValue();
257         if (contentEncoding == null) return responseStream;
258         if (contentEncoding.contains("gzip")) responseStream
259                 = new GZIPInputStream(responseStream);
260         return responseStream;
261     }
262 
263     /**
264      * Release resources associated with this client.  You must call this,
265      * or significant resources (sockets and memory) may be leaked.
266      */
close()267     public void close() {
268         if (mLeakedException != null) {
269             getConnectionManager().shutdown();
270             mLeakedException = null;
271         }
272     }
273 
getParams()274     public HttpParams getParams() {
275         return delegate.getParams();
276     }
277 
getConnectionManager()278     public ClientConnectionManager getConnectionManager() {
279         return delegate.getConnectionManager();
280     }
281 
execute(HttpUriRequest request)282     public HttpResponse execute(HttpUriRequest request) throws IOException {
283         return delegate.execute(request);
284     }
285 
execute(HttpUriRequest request, HttpContext context)286     public HttpResponse execute(HttpUriRequest request, HttpContext context)
287             throws IOException {
288         return delegate.execute(request, context);
289     }
290 
execute(HttpHost target, HttpRequest request)291     public HttpResponse execute(HttpHost target, HttpRequest request)
292             throws IOException {
293         return delegate.execute(target, request);
294     }
295 
execute(HttpHost target, HttpRequest request, HttpContext context)296     public HttpResponse execute(HttpHost target, HttpRequest request,
297             HttpContext context) throws IOException {
298         return delegate.execute(target, request, context);
299     }
300 
execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler)301     public <T> T execute(HttpUriRequest request,
302             ResponseHandler<? extends T> responseHandler)
303             throws IOException, ClientProtocolException {
304         return delegate.execute(request, responseHandler);
305     }
306 
execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler, HttpContext context)307     public <T> T execute(HttpUriRequest request,
308             ResponseHandler<? extends T> responseHandler, HttpContext context)
309             throws IOException, ClientProtocolException {
310         return delegate.execute(request, responseHandler, context);
311     }
312 
execute(HttpHost target, HttpRequest request, ResponseHandler<? extends T> responseHandler)313     public <T> T execute(HttpHost target, HttpRequest request,
314             ResponseHandler<? extends T> responseHandler) throws IOException,
315             ClientProtocolException {
316         return delegate.execute(target, request, responseHandler);
317     }
318 
execute(HttpHost target, HttpRequest request, ResponseHandler<? extends T> responseHandler, HttpContext context)319     public <T> T execute(HttpHost target, HttpRequest request,
320             ResponseHandler<? extends T> responseHandler, HttpContext context)
321             throws IOException, ClientProtocolException {
322         return delegate.execute(target, request, responseHandler, context);
323     }
324 
325     /**
326      * Compress data to send to server.
327      * Creates a Http Entity holding the gzipped data.
328      * The data will not be compressed if it is too short.
329      * @param data The bytes to compress
330      * @return Entity holding the data
331      */
getCompressedEntity(byte data[], ContentResolver resolver)332     public static AbstractHttpEntity getCompressedEntity(byte data[], ContentResolver resolver)
333             throws IOException {
334         AbstractHttpEntity entity;
335         if (data.length < getMinGzipSize(resolver)) {
336             entity = new ByteArrayEntity(data);
337         } else {
338             ByteArrayOutputStream arr = new ByteArrayOutputStream();
339             OutputStream zipper = new GZIPOutputStream(arr);
340             zipper.write(data);
341             zipper.close();
342             entity = new ByteArrayEntity(arr.toByteArray());
343             entity.setContentEncoding("gzip");
344         }
345         return entity;
346     }
347 
348     /**
349      * Retrieves the minimum size for compressing data.
350      * Shorter data will not be compressed.
351      */
getMinGzipSize(ContentResolver resolver)352     public static long getMinGzipSize(ContentResolver resolver) {
353         String sMinGzipBytes = Settings.Gservices.getString(resolver,
354                 Settings.Gservices.SYNC_MIN_GZIP_BYTES);
355 
356         if (!TextUtils.isEmpty(sMinGzipBytes)) {
357             try {
358                 return Long.parseLong(sMinGzipBytes);
359             } catch (NumberFormatException nfe) {
360                 Log.w(TAG, "Unable to parse " +
361                         Settings.Gservices.SYNC_MIN_GZIP_BYTES + " " +
362                         sMinGzipBytes, nfe);
363             }
364         }
365         return DEFAULT_SYNC_MIN_GZIP_BYTES;
366     }
367 
368     /* cURL logging support. */
369 
370     /**
371      * Logging tag and level.
372      */
373     private static class LoggingConfiguration {
374 
375         private final String tag;
376         private final int level;
377 
LoggingConfiguration(String tag, int level)378         private LoggingConfiguration(String tag, int level) {
379             this.tag = tag;
380             this.level = level;
381         }
382 
383         /**
384          * Returns true if logging is turned on for this configuration.
385          */
isLoggable()386         private boolean isLoggable() {
387             return Log.isLoggable(tag, level);
388         }
389 
390         /**
391          * Returns true if auth logging is turned on for this configuration.  Can only be set on
392          * insecure devices.
393          */
isAuthLoggable()394         private boolean isAuthLoggable() {
395             String secure = SystemProperties.get("ro.secure");
396             return "0".equals(secure) && Log.isLoggable(tag + "-auth", level);
397         }
398 
399         /**
400          * Prints a message using this configuration.
401          */
println(String message)402         private void println(String message) {
403             Log.println(level, tag, message);
404         }
405     }
406 
407     /** cURL logging configuration. */
408     private volatile LoggingConfiguration curlConfiguration;
409 
410     /**
411      * Enables cURL request logging for this client.
412      *
413      * @param name to log messages with
414      * @param level at which to log messages (see {@link android.util.Log})
415      */
enableCurlLogging(String name, int level)416     public void enableCurlLogging(String name, int level) {
417         if (name == null) {
418             throw new NullPointerException("name");
419         }
420         if (level < Log.VERBOSE || level > Log.ASSERT) {
421             throw new IllegalArgumentException("Level is out of range ["
422                 + Log.VERBOSE + ".." + Log.ASSERT + "]");
423         }
424 
425         curlConfiguration = new LoggingConfiguration(name, level);
426     }
427 
428     /**
429      * Disables cURL logging for this client.
430      */
disableCurlLogging()431     public void disableCurlLogging() {
432         curlConfiguration = null;
433     }
434 
435     /**
436      * Logs cURL commands equivalent to requests.
437      */
438     private class CurlLogger implements HttpRequestInterceptor {
process(HttpRequest request, HttpContext context)439         public void process(HttpRequest request, HttpContext context)
440                 throws HttpException, IOException {
441             LoggingConfiguration configuration = curlConfiguration;
442             if (configuration != null
443                     && configuration.isLoggable()
444                     && request instanceof HttpUriRequest) {
445                 configuration.println(toCurl((HttpUriRequest) request,
446                         configuration.isAuthLoggable()));
447             }
448         }
449     }
450 
451     /**
452      * Generates a cURL command equivalent to the given request.
453      */
toCurl(HttpUriRequest request, boolean logAuthToken)454     private static String toCurl(HttpUriRequest request, boolean logAuthToken) throws IOException {
455         StringBuilder builder = new StringBuilder();
456 
457         builder.append("curl ");
458 
459         for (Header header: request.getAllHeaders()) {
460             if (!logAuthToken
461                     && (header.getName().equals("Authorization") ||
462                         header.getName().equals("Cookie"))) {
463                 continue;
464             }
465             builder.append("--header \"");
466             builder.append(header.toString().trim());
467             builder.append("\" ");
468         }
469 
470         URI uri = request.getURI();
471 
472         // If this is a wrapped request, use the URI from the original
473         // request instead. getURI() on the wrapper seems to return a
474         // relative URI. We want an absolute URI.
475         if (request instanceof RequestWrapper) {
476             HttpRequest original = ((RequestWrapper) request).getOriginal();
477             if (original instanceof HttpUriRequest) {
478                 uri = ((HttpUriRequest) original).getURI();
479             }
480         }
481 
482         builder.append("\"");
483         builder.append(uri);
484         builder.append("\"");
485 
486         if (request instanceof HttpEntityEnclosingRequest) {
487             HttpEntityEnclosingRequest entityRequest =
488                     (HttpEntityEnclosingRequest) request;
489             HttpEntity entity = entityRequest.getEntity();
490             if (entity != null && entity.isRepeatable()) {
491                 if (entity.getContentLength() < 1024) {
492                     ByteArrayOutputStream stream = new ByteArrayOutputStream();
493                     entity.writeTo(stream);
494                     String entityString = stream.toString();
495 
496                     // TODO: Check the content type, too.
497                     builder.append(" --data-ascii \"")
498                             .append(entityString)
499                             .append("\"");
500                 } else {
501                     builder.append(" [TOO MUCH DATA TO INCLUDE]");
502                 }
503             }
504         }
505 
506         return builder.toString();
507     }
508 }
509