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