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