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