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