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