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