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