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