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