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