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