AndroidHttpClient.java revision 823675fdbb7f974b8e2fa9fbb71774b32487582d
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
17package android.net.http;
18
19import com.android.internal.http.HttpDateTime;
20
21import org.apache.http.Header;
22import org.apache.http.HttpEntity;
23import org.apache.http.HttpEntityEnclosingRequest;
24import org.apache.http.HttpException;
25import org.apache.http.HttpHost;
26import org.apache.http.HttpRequest;
27import org.apache.http.HttpRequestInterceptor;
28import org.apache.http.HttpResponse;
29import org.apache.http.client.ClientProtocolException;
30import org.apache.http.client.HttpClient;
31import org.apache.http.client.ResponseHandler;
32import org.apache.http.client.methods.HttpUriRequest;
33import org.apache.http.client.params.HttpClientParams;
34import org.apache.http.client.protocol.ClientContext;
35import org.apache.http.conn.ClientConnectionManager;
36import org.apache.http.conn.scheme.PlainSocketFactory;
37import org.apache.http.conn.scheme.Scheme;
38import org.apache.http.conn.scheme.SchemeRegistry;
39import org.apache.http.entity.AbstractHttpEntity;
40import org.apache.http.entity.ByteArrayEntity;
41import org.apache.http.impl.client.DefaultHttpClient;
42import org.apache.http.impl.client.RequestWrapper;
43import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
44import org.apache.http.params.BasicHttpParams;
45import org.apache.http.params.HttpConnectionParams;
46import org.apache.http.params.HttpParams;
47import org.apache.http.params.HttpProtocolParams;
48import org.apache.http.protocol.BasicHttpContext;
49import org.apache.http.protocol.BasicHttpProcessor;
50import org.apache.http.protocol.HttpContext;
51
52import android.content.ContentResolver;
53import android.content.Context;
54import android.net.SSLCertificateSocketFactory;
55import android.net.SSLSessionCache;
56import android.os.Looper;
57import android.util.Base64;
58import android.util.Log;
59
60import java.io.ByteArrayOutputStream;
61import java.io.IOException;
62import java.io.InputStream;
63import java.io.OutputStream;
64import java.net.URI;
65import java.util.zip.GZIPInputStream;
66import 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
84public 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
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
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
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
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     */
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     */
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     */
254    public void close() {
255        if (mLeakedException != null) {
256            getConnectionManager().shutdown();
257            mLeakedException = null;
258        }
259    }
260
261    public HttpParams getParams() {
262        return delegate.getParams();
263    }
264
265    public ClientConnectionManager getConnectionManager() {
266        return delegate.getConnectionManager();
267    }
268
269    public HttpResponse execute(HttpUriRequest request) throws IOException {
270        return delegate.execute(request);
271    }
272
273    public HttpResponse execute(HttpUriRequest request, HttpContext context)
274            throws IOException {
275        return delegate.execute(request, context);
276    }
277
278    public HttpResponse execute(HttpHost target, HttpRequest request)
279            throws IOException {
280        return delegate.execute(target, request);
281    }
282
283    public HttpResponse execute(HttpHost target, HttpRequest request,
284            HttpContext context) throws IOException {
285        return delegate.execute(target, request, context);
286    }
287
288    public <T> T execute(HttpUriRequest request,
289            ResponseHandler<? extends T> responseHandler)
290            throws IOException, ClientProtocolException {
291        return delegate.execute(request, responseHandler);
292    }
293
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
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
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     */
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     */
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
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         */
361        private boolean isLoggable() {
362            return Log.isLoggable(tag, level);
363        }
364
365        /**
366         * Prints a message using this configuration.
367         */
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     */
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     */
397    public void disableCurlLogging() {
398        curlConfiguration = null;
399    }
400
401    /**
402     * Logs cURL commands equivalent to requests.
403     */
404    private class CurlLogger implements HttpRequestInterceptor {
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     */
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
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     */
524    public static long parseDate(String dateString) {
525        return HttpDateTime.parse(dateString);
526    }
527}
528