• 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 com.android.internal.http.HttpDateTime;
20 
21 import org.apache.http.Header;
22 import org.apache.http.HttpEntity;
23 import org.apache.http.HttpEntityEnclosingRequest;
24 import org.apache.http.HttpException;
25 import org.apache.http.HttpHost;
26 import org.apache.http.HttpRequest;
27 import org.apache.http.HttpRequestInterceptor;
28 import org.apache.http.HttpResponse;
29 import org.apache.http.client.ClientProtocolException;
30 import org.apache.http.client.HttpClient;
31 import org.apache.http.client.ResponseHandler;
32 import org.apache.http.client.methods.HttpUriRequest;
33 import org.apache.http.client.params.HttpClientParams;
34 import org.apache.http.client.protocol.ClientContext;
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.entity.AbstractHttpEntity;
40 import org.apache.http.entity.ByteArrayEntity;
41 import org.apache.http.impl.client.DefaultHttpClient;
42 import org.apache.http.impl.client.RequestWrapper;
43 import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
44 import org.apache.http.params.BasicHttpParams;
45 import org.apache.http.params.HttpConnectionParams;
46 import org.apache.http.params.HttpParams;
47 import org.apache.http.params.HttpProtocolParams;
48 import org.apache.http.protocol.BasicHttpContext;
49 import org.apache.http.protocol.BasicHttpProcessor;
50 import org.apache.http.protocol.HttpContext;
51 
52 import android.content.ContentResolver;
53 import android.content.Context;
54 import android.net.SSLCertificateSocketFactory;
55 import android.net.SSLSessionCache;
56 import android.os.Looper;
57 import android.util.Base64;
58 import android.util.Log;
59 
60 import java.io.ByteArrayOutputStream;
61 import java.io.IOException;
62 import java.io.InputStream;
63 import java.io.OutputStream;
64 import java.net.URI;
65 import java.util.zip.GZIPInputStream;
66 import java.util.zip.GZIPOutputStream;
67 
68 /**
69  * Implementation of the Apache {@link DefaultHttpClient} that is configured with
70  * reasonable default settings and registered schemes for Android.
71  * Don't create this directly, use the {@link #newInstance} factory method.
72  *
73  * <p>This client processes cookies but does not retain them by default.
74  * To retain cookies, simply add a cookie store to the HttpContext:</p>
75  *
76  * <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre>
77  */
78 public final class AndroidHttpClient implements HttpClient {
79 
80     // Gzip of data shorter than this probably won't be worthwhile
81     public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256;
82 
83     // Default connection and socket timeout of 60 seconds.  Tweak to taste.
84     private static final int SOCKET_OPERATION_TIMEOUT = 60 * 1000;
85 
86     private static final String TAG = "AndroidHttpClient";
87 
88     private static String[] textContentTypes = new String[] {
89             "text/",
90             "application/xml",
91             "application/json"
92     };
93 
94     /** Interceptor throws an exception if the executing thread is blocked */
95     private static final HttpRequestInterceptor sThreadCheckInterceptor =
96             new HttpRequestInterceptor() {
97         public void process(HttpRequest request, HttpContext context) {
98             // Prevent the HttpRequest from being sent on the main thread
99             if (Looper.myLooper() != null && Looper.myLooper() == Looper.getMainLooper() ) {
100                 throw new RuntimeException("This thread forbids HTTP requests");
101             }
102         }
103     };
104 
105     /**
106      * Create a new HttpClient with reasonable defaults (which you can update).
107      *
108      * @param userAgent to report in your HTTP requests
109      * @param context to use for caching SSL sessions (may be null for no caching)
110      * @return AndroidHttpClient for you to use for all your requests.
111      */
newInstance(String userAgent, Context context)112     public static AndroidHttpClient newInstance(String userAgent, Context context) {
113         HttpParams params = new BasicHttpParams();
114 
115         // Turn off stale checking.  Our connections break all the time anyway,
116         // and it's not worth it to pay the penalty of checking every time.
117         HttpConnectionParams.setStaleCheckingEnabled(params, false);
118 
119         HttpConnectionParams.setConnectionTimeout(params, SOCKET_OPERATION_TIMEOUT);
120         HttpConnectionParams.setSoTimeout(params, SOCKET_OPERATION_TIMEOUT);
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         // Use a session cache for SSL sockets
128         SSLSessionCache sessionCache = context == null ? null : new SSLSessionCache(context);
129 
130         // Set the specified user agent and register standard protocols.
131         HttpProtocolParams.setUserAgent(params, userAgent);
132         SchemeRegistry schemeRegistry = new SchemeRegistry();
133         schemeRegistry.register(new Scheme("http",
134                 PlainSocketFactory.getSocketFactory(), 80));
135         schemeRegistry.register(new Scheme("https",
136                 SSLCertificateSocketFactory.getHttpSocketFactory(
137                 SOCKET_OPERATION_TIMEOUT, sessionCache), 443));
138 
139         ClientConnectionManager manager =
140                 new ThreadSafeClientConnManager(params, schemeRegistry);
141 
142         // We use a factory method to modify superclass initialization
143         // parameters without the funny call-a-static-method dance.
144         return new AndroidHttpClient(manager, params);
145     }
146 
147     /**
148      * Create a new HttpClient with reasonable defaults (which you can update).
149      * @param userAgent to report in your HTTP requests.
150      * @return AndroidHttpClient for you to use for all your requests.
151      */
newInstance(String userAgent)152     public static AndroidHttpClient newInstance(String userAgent) {
153         return newInstance(userAgent, null /* session cache */);
154     }
155 
156     private final HttpClient delegate;
157 
158     private RuntimeException mLeakedException = new IllegalStateException(
159             "AndroidHttpClient created and never closed");
160 
AndroidHttpClient(ClientConnectionManager ccm, HttpParams params)161     private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) {
162         this.delegate = new DefaultHttpClient(ccm, params) {
163             @Override
164             protected BasicHttpProcessor createHttpProcessor() {
165                 // Add interceptor to prevent making requests from main thread.
166                 BasicHttpProcessor processor = super.createHttpProcessor();
167                 processor.addRequestInterceptor(sThreadCheckInterceptor);
168                 processor.addRequestInterceptor(new CurlLogger());
169 
170                 return processor;
171             }
172 
173             @Override
174             protected HttpContext createHttpContext() {
175                 // Same as DefaultHttpClient.createHttpContext() minus the
176                 // cookie store.
177                 HttpContext context = new BasicHttpContext();
178                 context.setAttribute(
179                         ClientContext.AUTHSCHEME_REGISTRY,
180                         getAuthSchemes());
181                 context.setAttribute(
182                         ClientContext.COOKIESPEC_REGISTRY,
183                         getCookieSpecs());
184                 context.setAttribute(
185                         ClientContext.CREDS_PROVIDER,
186                         getCredentialsProvider());
187                 return context;
188             }
189         };
190     }
191 
192     @Override
finalize()193     protected void finalize() throws Throwable {
194         super.finalize();
195         if (mLeakedException != null) {
196             Log.e(TAG, "Leak found", mLeakedException);
197             mLeakedException = null;
198         }
199     }
200 
201     /**
202      * Modifies a request to indicate to the server that we would like a
203      * gzipped response.  (Uses the "Accept-Encoding" HTTP header.)
204      * @param request the request to modify
205      * @see #getUngzippedContent
206      */
modifyRequestToAcceptGzipResponse(HttpRequest request)207     public static void modifyRequestToAcceptGzipResponse(HttpRequest request) {
208         request.addHeader("Accept-Encoding", "gzip");
209     }
210 
211     /**
212      * Gets the input stream from a response entity.  If the entity is gzipped
213      * then this will get a stream over the uncompressed data.
214      *
215      * @param entity the entity whose content should be read
216      * @return the input stream to read from
217      * @throws IOException
218      */
getUngzippedContent(HttpEntity entity)219     public static InputStream getUngzippedContent(HttpEntity entity)
220             throws IOException {
221         InputStream responseStream = entity.getContent();
222         if (responseStream == null) return responseStream;
223         Header header = entity.getContentEncoding();
224         if (header == null) return responseStream;
225         String contentEncoding = header.getValue();
226         if (contentEncoding == null) return responseStream;
227         if (contentEncoding.contains("gzip")) responseStream
228                 = new GZIPInputStream(responseStream);
229         return responseStream;
230     }
231 
232     /**
233      * Release resources associated with this client.  You must call this,
234      * or significant resources (sockets and memory) may be leaked.
235      */
close()236     public void close() {
237         if (mLeakedException != null) {
238             getConnectionManager().shutdown();
239             mLeakedException = null;
240         }
241     }
242 
getParams()243     public HttpParams getParams() {
244         return delegate.getParams();
245     }
246 
getConnectionManager()247     public ClientConnectionManager getConnectionManager() {
248         return delegate.getConnectionManager();
249     }
250 
execute(HttpUriRequest request)251     public HttpResponse execute(HttpUriRequest request) throws IOException {
252         return delegate.execute(request);
253     }
254 
execute(HttpUriRequest request, HttpContext context)255     public HttpResponse execute(HttpUriRequest request, HttpContext context)
256             throws IOException {
257         return delegate.execute(request, context);
258     }
259 
execute(HttpHost target, HttpRequest request)260     public HttpResponse execute(HttpHost target, HttpRequest request)
261             throws IOException {
262         return delegate.execute(target, request);
263     }
264 
execute(HttpHost target, HttpRequest request, HttpContext context)265     public HttpResponse execute(HttpHost target, HttpRequest request,
266             HttpContext context) throws IOException {
267         return delegate.execute(target, request, context);
268     }
269 
execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler)270     public <T> T execute(HttpUriRequest request,
271             ResponseHandler<? extends T> responseHandler)
272             throws IOException, ClientProtocolException {
273         return delegate.execute(request, responseHandler);
274     }
275 
execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler, HttpContext context)276     public <T> T execute(HttpUriRequest request,
277             ResponseHandler<? extends T> responseHandler, HttpContext context)
278             throws IOException, ClientProtocolException {
279         return delegate.execute(request, responseHandler, context);
280     }
281 
execute(HttpHost target, HttpRequest request, ResponseHandler<? extends T> responseHandler)282     public <T> T execute(HttpHost target, HttpRequest request,
283             ResponseHandler<? extends T> responseHandler) throws IOException,
284             ClientProtocolException {
285         return delegate.execute(target, request, responseHandler);
286     }
287 
execute(HttpHost target, HttpRequest request, ResponseHandler<? extends T> responseHandler, HttpContext context)288     public <T> T execute(HttpHost target, HttpRequest request,
289             ResponseHandler<? extends T> responseHandler, HttpContext context)
290             throws IOException, ClientProtocolException {
291         return delegate.execute(target, request, responseHandler, context);
292     }
293 
294     /**
295      * Compress data to send to server.
296      * Creates a Http Entity holding the gzipped data.
297      * The data will not be compressed if it is too short.
298      * @param data The bytes to compress
299      * @return Entity holding the data
300      */
getCompressedEntity(byte data[], ContentResolver resolver)301     public static AbstractHttpEntity getCompressedEntity(byte data[], ContentResolver resolver)
302             throws IOException {
303         AbstractHttpEntity entity;
304         if (data.length < getMinGzipSize(resolver)) {
305             entity = new ByteArrayEntity(data);
306         } else {
307             ByteArrayOutputStream arr = new ByteArrayOutputStream();
308             OutputStream zipper = new GZIPOutputStream(arr);
309             zipper.write(data);
310             zipper.close();
311             entity = new ByteArrayEntity(arr.toByteArray());
312             entity.setContentEncoding("gzip");
313         }
314         return entity;
315     }
316 
317     /**
318      * Retrieves the minimum size for compressing data.
319      * Shorter data will not be compressed.
320      */
getMinGzipSize(ContentResolver resolver)321     public static long getMinGzipSize(ContentResolver resolver) {
322         return DEFAULT_SYNC_MIN_GZIP_BYTES;  // For now, this is just a constant.
323     }
324 
325     /* cURL logging support. */
326 
327     /**
328      * Logging tag and level.
329      */
330     private static class LoggingConfiguration {
331 
332         private final String tag;
333         private final int level;
334 
LoggingConfiguration(String tag, int level)335         private LoggingConfiguration(String tag, int level) {
336             this.tag = tag;
337             this.level = level;
338         }
339 
340         /**
341          * Returns true if logging is turned on for this configuration.
342          */
isLoggable()343         private boolean isLoggable() {
344             return Log.isLoggable(tag, level);
345         }
346 
347         /**
348          * Prints a message using this configuration.
349          */
println(String message)350         private void println(String message) {
351             Log.println(level, tag, message);
352         }
353     }
354 
355     /** cURL logging configuration. */
356     private volatile LoggingConfiguration curlConfiguration;
357 
358     /**
359      * Enables cURL request logging for this client.
360      *
361      * @param name to log messages with
362      * @param level at which to log messages (see {@link android.util.Log})
363      */
enableCurlLogging(String name, int level)364     public void enableCurlLogging(String name, int level) {
365         if (name == null) {
366             throw new NullPointerException("name");
367         }
368         if (level < Log.VERBOSE || level > Log.ASSERT) {
369             throw new IllegalArgumentException("Level is out of range ["
370                 + Log.VERBOSE + ".." + Log.ASSERT + "]");
371         }
372 
373         curlConfiguration = new LoggingConfiguration(name, level);
374     }
375 
376     /**
377      * Disables cURL logging for this client.
378      */
disableCurlLogging()379     public void disableCurlLogging() {
380         curlConfiguration = null;
381     }
382 
383     /**
384      * Logs cURL commands equivalent to requests.
385      */
386     private class CurlLogger implements HttpRequestInterceptor {
process(HttpRequest request, HttpContext context)387         public void process(HttpRequest request, HttpContext context)
388                 throws HttpException, IOException {
389             LoggingConfiguration configuration = curlConfiguration;
390             if (configuration != null
391                     && configuration.isLoggable()
392                     && request instanceof HttpUriRequest) {
393                 // Never print auth token -- we used to check ro.secure=0 to
394                 // enable that, but can't do that in unbundled code.
395                 configuration.println(toCurl((HttpUriRequest) request, false));
396             }
397         }
398     }
399 
400     /**
401      * Generates a cURL command equivalent to the given request.
402      */
toCurl(HttpUriRequest request, boolean logAuthToken)403     private static String toCurl(HttpUriRequest request, boolean logAuthToken) throws IOException {
404         StringBuilder builder = new StringBuilder();
405 
406         builder.append("curl ");
407 
408         // add in the method
409         builder.append("-X ");
410         builder.append(request.getMethod());
411         builder.append(" ");
412 
413         for (Header header: request.getAllHeaders()) {
414             if (!logAuthToken
415                     && (header.getName().equals("Authorization") ||
416                         header.getName().equals("Cookie"))) {
417                 continue;
418             }
419             builder.append("--header \"");
420             builder.append(header.toString().trim());
421             builder.append("\" ");
422         }
423 
424         URI uri = request.getURI();
425 
426         // If this is a wrapped request, use the URI from the original
427         // request instead. getURI() on the wrapper seems to return a
428         // relative URI. We want an absolute URI.
429         if (request instanceof RequestWrapper) {
430             HttpRequest original = ((RequestWrapper) request).getOriginal();
431             if (original instanceof HttpUriRequest) {
432                 uri = ((HttpUriRequest) original).getURI();
433             }
434         }
435 
436         builder.append("\"");
437         builder.append(uri);
438         builder.append("\"");
439 
440         if (request instanceof HttpEntityEnclosingRequest) {
441             HttpEntityEnclosingRequest entityRequest =
442                     (HttpEntityEnclosingRequest) request;
443             HttpEntity entity = entityRequest.getEntity();
444             if (entity != null && entity.isRepeatable()) {
445                 if (entity.getContentLength() < 1024) {
446                     ByteArrayOutputStream stream = new ByteArrayOutputStream();
447                     entity.writeTo(stream);
448 
449                     if (isBinaryContent(request)) {
450                         String base64 = Base64.encodeToString(stream.toByteArray(), Base64.NO_WRAP);
451                         builder.insert(0, "echo '" + base64 + "' | base64 -d > /tmp/$$.bin; ");
452                         builder.append(" --data-binary @/tmp/$$.bin");
453                     } else {
454                         String entityString = stream.toString();
455                         builder.append(" --data-ascii \"")
456                                 .append(entityString)
457                                 .append("\"");
458                     }
459                 } else {
460                     builder.append(" [TOO MUCH DATA TO INCLUDE]");
461                 }
462             }
463         }
464 
465         return builder.toString();
466     }
467 
isBinaryContent(HttpUriRequest request)468     private static boolean isBinaryContent(HttpUriRequest request) {
469         Header[] headers;
470         headers = request.getHeaders(Headers.CONTENT_ENCODING);
471         if (headers != null) {
472             for (Header header : headers) {
473                 if ("gzip".equalsIgnoreCase(header.getValue())) {
474                     return true;
475                 }
476             }
477         }
478 
479         headers = request.getHeaders(Headers.CONTENT_TYPE);
480         if (headers != null) {
481             for (Header header : headers) {
482                 for (String contentType : textContentTypes) {
483                     if (header.getValue().startsWith(contentType)) {
484                         return false;
485                     }
486                 }
487             }
488         }
489         return true;
490     }
491 
492     /**
493      * Returns the date of the given HTTP date string. This method can identify
494      * and parse the date formats emitted by common HTTP servers, such as
495      * <a href="http://www.ietf.org/rfc/rfc0822.txt">RFC 822</a>,
496      * <a href="http://www.ietf.org/rfc/rfc0850.txt">RFC 850</a>,
497      * <a href="http://www.ietf.org/rfc/rfc1036.txt">RFC 1036</a>,
498      * <a href="http://www.ietf.org/rfc/rfc1123.txt">RFC 1123</a> and
499      * <a href="http://www.opengroup.org/onlinepubs/007908799/xsh/asctime.html">ANSI
500      * C's asctime()</a>.
501      *
502      * @return the number of milliseconds since Jan. 1, 1970, midnight GMT.
503      * @throws IllegalArgumentException if {@code dateString} is not a date or
504      *     of an unsupported format.
505      */
parseDate(String dateString)506     public static long parseDate(String dateString) {
507         return HttpDateTime.parse(dateString);
508     }
509 }
510