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