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