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 org.apache.http.Header;
20import org.apache.http.HttpEntity;
21import org.apache.http.HttpEntityEnclosingRequest;
22import org.apache.http.HttpException;
23import org.apache.http.HttpHost;
24import org.apache.http.HttpRequest;
25import org.apache.http.HttpRequestInterceptor;
26import org.apache.http.HttpResponse;
27import org.apache.http.client.ClientProtocolException;
28import org.apache.http.client.HttpClient;
29import org.apache.http.client.ResponseHandler;
30import org.apache.http.client.methods.HttpUriRequest;
31import org.apache.http.client.params.HttpClientParams;
32import org.apache.http.client.protocol.ClientContext;
33import org.apache.http.conn.ClientConnectionManager;
34import org.apache.http.conn.scheme.PlainSocketFactory;
35import org.apache.http.conn.scheme.Scheme;
36import org.apache.http.conn.scheme.SchemeRegistry;
37import org.apache.http.entity.AbstractHttpEntity;
38import org.apache.http.entity.ByteArrayEntity;
39import org.apache.http.impl.client.DefaultHttpClient;
40import org.apache.http.impl.client.RequestWrapper;
41import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
42import org.apache.http.params.BasicHttpParams;
43import org.apache.http.params.HttpConnectionParams;
44import org.apache.http.params.HttpParams;
45import org.apache.http.params.HttpProtocolParams;
46import org.apache.http.protocol.BasicHttpContext;
47import org.apache.http.protocol.BasicHttpProcessor;
48import org.apache.http.protocol.HttpContext;
49
50import android.content.ContentResolver;
51import android.content.Context;
52import android.net.SSLCertificateSocketFactory;
53import android.net.SSLSessionCache;
54import android.os.Looper;
55import android.util.Base64;
56import android.util.Log;
57
58import java.io.ByteArrayOutputStream;
59import java.io.IOException;
60import java.io.InputStream;
61import java.io.OutputStream;
62import java.net.URI;
63import java.util.zip.GZIPInputStream;
64import 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 */
76public 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     */
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     */
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
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
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     */
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     */
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     */
234    public void close() {
235        if (mLeakedException != null) {
236            getConnectionManager().shutdown();
237            mLeakedException = null;
238        }
239    }
240
241    public HttpParams getParams() {
242        return delegate.getParams();
243    }
244
245    public ClientConnectionManager getConnectionManager() {
246        return delegate.getConnectionManager();
247    }
248
249    public HttpResponse execute(HttpUriRequest request) throws IOException {
250        return delegate.execute(request);
251    }
252
253    public HttpResponse execute(HttpUriRequest request, HttpContext context)
254            throws IOException {
255        return delegate.execute(request, context);
256    }
257
258    public HttpResponse execute(HttpHost target, HttpRequest request)
259            throws IOException {
260        return delegate.execute(target, request);
261    }
262
263    public HttpResponse execute(HttpHost target, HttpRequest request,
264            HttpContext context) throws IOException {
265        return delegate.execute(target, request, context);
266    }
267
268    public <T> T execute(HttpUriRequest request,
269            ResponseHandler<? extends T> responseHandler)
270            throws IOException, ClientProtocolException {
271        return delegate.execute(request, responseHandler);
272    }
273
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
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
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     */
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     */
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
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         */
341        private boolean isLoggable() {
342            return Log.isLoggable(tag, level);
343        }
344
345        /**
346         * Prints a message using this configuration.
347         */
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     */
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     */
377    public void disableCurlLogging() {
378        curlConfiguration = null;
379    }
380
381    /**
382     * Logs cURL commands equivalent to requests.
383     */
384    private class CurlLogger implements HttpRequestInterceptor {
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     */
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
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     */
504    public static long parseDate(String dateString) {
505        return LegacyHttpDateTime.parse(dateString);
506    }
507}
508