1/* 2 * Copyright (C) 2008 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 com.google.android.net; 18 19import android.content.ContentResolver; 20import android.content.ContentValues; 21import android.content.Context; 22import android.net.http.AndroidHttpClient; 23import android.os.Build; 24import android.os.NetStat; 25import android.os.SystemClock; 26import android.provider.Checkin; 27import android.util.Config; 28import android.util.Log; 29import org.apache.http.HttpEntity; 30import org.apache.http.HttpEntityEnclosingRequest; 31import org.apache.http.HttpHost; 32import org.apache.http.HttpRequest; 33import org.apache.http.HttpResponse; 34import org.apache.http.ProtocolException; 35import org.apache.http.client.ClientProtocolException; 36import org.apache.http.client.HttpClient; 37import org.apache.http.client.ResponseHandler; 38import org.apache.http.client.methods.HttpUriRequest; 39import org.apache.http.conn.ClientConnectionManager; 40import org.apache.http.conn.scheme.LayeredSocketFactory; 41import org.apache.http.conn.scheme.Scheme; 42import org.apache.http.conn.scheme.SchemeRegistry; 43import org.apache.http.conn.scheme.SocketFactory; 44import org.apache.http.impl.client.EntityEnclosingRequestWrapper; 45import org.apache.http.impl.client.RequestWrapper; 46import org.apache.http.params.HttpParams; 47import org.apache.http.protocol.HttpContext; 48import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache; 49 50import java.io.IOException; 51import java.net.InetAddress; 52import java.net.Socket; 53import java.net.URI; 54import java.net.URISyntaxException; 55 56/** 57 * {@link AndroidHttpClient} wrapper that uses {@link UrlRules} to rewrite URLs 58 * and otherwise tweak HTTP requests. 59 */ 60public class GoogleHttpClient implements HttpClient { 61 private static final String TAG = "GoogleHttpClient"; 62 private static final boolean LOCAL_LOGV = Config.LOGV || false; 63 64 /** Exception thrown when a request is blocked by the URL rules. */ 65 public static class BlockedRequestException extends IOException { 66 private final UrlRules.Rule mRule; 67 BlockedRequestException(UrlRules.Rule rule) { 68 super("Blocked by rule: " + rule.mName); 69 mRule = rule; 70 } 71 } 72 73 private final AndroidHttpClient mClient; 74 private final ContentResolver mResolver; 75 private final String mAppName, mUserAgent; 76 private final ThreadLocal<Boolean> mConnectionAllocated = new ThreadLocal<Boolean>(); 77 78 /** 79 * Create an HTTP client without SSL session persistence. 80 * @deprecated Use {@link #GoogleHttpClient(android.content.Context, String, boolean)} 81 */ 82 public GoogleHttpClient(ContentResolver resolver, String userAgent) { 83 mClient = AndroidHttpClient.newInstance(userAgent); 84 mResolver = resolver; 85 mUserAgent = mAppName = userAgent; 86 } 87 88 /** 89 * Create an HTTP client without SSL session persistence. 90 * @deprecated Use {@link #GoogleHttpClient(android.content.Context, String, boolean)} 91 */ 92 public GoogleHttpClient(ContentResolver resolver, String appAndVersion, 93 boolean gzipCapable) { 94 this(resolver, null /* cache */, appAndVersion, gzipCapable); 95 } 96 97 /** 98 * Create an HTTP client. Normaly this client is shared throughout an app. 99 * The HTTP client will construct its User-Agent as follows: 100 * 101 * <appAndVersion> (<build device> <build id>) 102 * or 103 * <appAndVersion> (<build device> <build id>); gzip 104 * (if gzip capable) 105 * 106 * The context has settings for URL rewriting rules and is used to enable 107 * SSL session persistence. 108 * 109 * @param context application context. 110 * @param appAndVersion Base app and version to use in the User-Agent. 111 * e.g., "MyApp/1.0" 112 * @param gzipCapable Whether or not this client is able to consume gzip'd 113 * responses. Only used to modify the User-Agent, not other request 114 * headers. Needed because Google servers require gzip in the User-Agent 115 * in order to return gzip'd content. 116 */ 117 public GoogleHttpClient(Context context, String appAndVersion, boolean gzipCapable) { 118 this(context.getContentResolver(), 119 SSLClientSessionCacheFactory.getCache(context), 120 appAndVersion, gzipCapable); 121 } 122 123 private GoogleHttpClient(ContentResolver resolver, 124 SSLClientSessionCache cache, 125 String appAndVersion, boolean gzipCapable) { 126 String userAgent = appAndVersion + " (" + Build.DEVICE + " " + Build.ID + ")"; 127 if (gzipCapable) { 128 userAgent = userAgent + "; gzip"; 129 } 130 131 mClient = AndroidHttpClient.newInstance(userAgent, cache); 132 mResolver = resolver; 133 mAppName = appAndVersion; 134 mUserAgent = userAgent; 135 136 // Wrap all the socket factories with the appropriate wrapper. (Apache 137 // HTTP, curse its black and stupid heart, inspects the SocketFactory to 138 // see if it's a LayeredSocketFactory, so we need two wrapper classes.) 139 SchemeRegistry registry = getConnectionManager().getSchemeRegistry(); 140 for (String name : registry.getSchemeNames()) { 141 Scheme scheme = registry.unregister(name); 142 SocketFactory sf = scheme.getSocketFactory(); 143 if (sf instanceof LayeredSocketFactory) { 144 sf = new WrappedLayeredSocketFactory((LayeredSocketFactory) sf); 145 } else { 146 sf = new WrappedSocketFactory(sf); 147 } 148 registry.register(new Scheme(name, sf, scheme.getDefaultPort())); 149 } 150 } 151 152 /** 153 * Delegating wrapper for SocketFactory records when sockets are connected. 154 * We use this to know whether a connection was created vs reused, to 155 * gather per-app statistics about connection reuse rates. 156 * (Note, we record only *connection*, not *creation* of sockets -- 157 * what we care about is the network overhead of an actual TCP connect.) 158 */ 159 private class WrappedSocketFactory implements SocketFactory { 160 private SocketFactory mDelegate; 161 private WrappedSocketFactory(SocketFactory delegate) { mDelegate = delegate; } 162 public final Socket createSocket() throws IOException { return mDelegate.createSocket(); } 163 public final boolean isSecure(Socket s) { return mDelegate.isSecure(s); } 164 165 public final Socket connectSocket( 166 Socket s, String h, int p, 167 InetAddress la, int lp, HttpParams params) throws IOException { 168 mConnectionAllocated.set(Boolean.TRUE); 169 return mDelegate.connectSocket(s, h, p, la, lp, params); 170 } 171 } 172 173 /** Like WrappedSocketFactory, but for the LayeredSocketFactory subclass. */ 174 private class WrappedLayeredSocketFactory 175 extends WrappedSocketFactory implements LayeredSocketFactory { 176 private LayeredSocketFactory mDelegate; 177 private WrappedLayeredSocketFactory(LayeredSocketFactory sf) { super(sf); mDelegate = sf; } 178 179 public final Socket createSocket(Socket s, String host, int port, boolean autoClose) 180 throws IOException { 181 return mDelegate.createSocket(s, host, port, autoClose); 182 } 183 } 184 185 /** 186 * Release resources associated with this client. You must call this, 187 * or significant resources (sockets and memory) may be leaked. 188 */ 189 public void close() { 190 mClient.close(); 191 } 192 193 /** Execute a request without applying and rewrite rules. */ 194 public HttpResponse executeWithoutRewriting( 195 HttpUriRequest request, HttpContext context) 196 throws IOException { 197 int code = -1; 198 long start = SystemClock.elapsedRealtime(); 199 try { 200 HttpResponse response; 201 mConnectionAllocated.set(null); 202 203 if (NetworkStatsEntity.shouldLogNetworkStats()) { 204 // TODO: if we're logging network stats, and if the apache library is configured 205 // to follow redirects, count each redirect as an additional round trip. 206 207 int uid = android.os.Process.myUid(); 208 long startTx = NetStat.getUidTxBytes(uid); 209 long startRx = NetStat.getUidRxBytes(uid); 210 211 response = mClient.execute(request, context); 212 HttpEntity origEntity = response == null ? null : response.getEntity(); 213 if (origEntity != null) { 214 // yeah, we compute the same thing below. we do need to compute this here 215 // so we can wrap the HttpEntity in the response. 216 long now = SystemClock.elapsedRealtime(); 217 long elapsed = now - start; 218 NetworkStatsEntity entity = new NetworkStatsEntity(origEntity, 219 mAppName, uid, startTx, startRx, 220 elapsed /* response latency */, now /* processing start time */); 221 response.setEntity(entity); 222 } 223 } else { 224 response = mClient.execute(request, context); 225 } 226 227 code = response.getStatusLine().getStatusCode(); 228 return response; 229 } finally { 230 // Record some statistics to the checkin service about the outcome. 231 // Note that this is only describing execute(), not body download. 232 // We assume the database writes are much faster than network I/O, 233 // and not worth running in a background thread or anything. 234 try { 235 long elapsed = SystemClock.elapsedRealtime() - start; 236 ContentValues values = new ContentValues(); 237 values.put(Checkin.Stats.COUNT, 1); 238 values.put(Checkin.Stats.SUM, elapsed / 1000.0); 239 240 values.put(Checkin.Stats.TAG, Checkin.Stats.Tag.HTTP_REQUEST + ":" + mAppName); 241 mResolver.insert(Checkin.Stats.CONTENT_URI, values); 242 243 // No sockets and no exceptions means we successfully reused a connection 244 if (mConnectionAllocated.get() == null && code >= 0) { 245 values.put(Checkin.Stats.TAG, Checkin.Stats.Tag.HTTP_REUSED + ":" + mAppName); 246 mResolver.insert(Checkin.Stats.CONTENT_URI, values); 247 } 248 249 String status = code < 0 ? "IOException" : Integer.toString(code); 250 values.put(Checkin.Stats.TAG, 251 Checkin.Stats.Tag.HTTP_STATUS + ":" + mAppName + ":" + status); 252 mResolver.insert(Checkin.Stats.CONTENT_URI, values); 253 } catch (Exception e) { 254 Log.e(TAG, "Error recording stats", e); 255 } 256 } 257 } 258 259 public String rewriteURI(String original) { 260 UrlRules rules = UrlRules.getRules(mResolver); 261 UrlRules.Rule rule = rules.matchRule(original); 262 return rule.apply(original); 263 } 264 265 public HttpResponse execute(HttpUriRequest request, HttpContext context) 266 throws IOException { 267 // Rewrite the supplied URL... 268 URI uri = request.getURI(); 269 String original = uri.toString(); 270 UrlRules rules = UrlRules.getRules(mResolver); 271 UrlRules.Rule rule = rules.matchRule(original); 272 String rewritten = rule.apply(original); 273 274 if (rewritten == null) { 275 Log.w(TAG, "Blocked by " + rule.mName + ": " + original); 276 throw new BlockedRequestException(rule); 277 } else if (rewritten == original) { 278 return executeWithoutRewriting(request, context); // Pass through 279 } 280 281 try { 282 uri = new URI(rewritten); 283 } catch (URISyntaxException e) { 284 throw new RuntimeException("Bad URL from rule: " + rule.mName, e); 285 } 286 287 // Wrap request so we can replace the URI. 288 RequestWrapper wrapper = wrapRequest(request); 289 wrapper.setURI(uri); 290 request = wrapper; 291 292 if (LOCAL_LOGV) Log.v(TAG, "Rule " + rule.mName + ": " + original + " -> " + rewritten); 293 return executeWithoutRewriting(request, context); 294 } 295 296 /** 297 * Wraps the request making it mutable. 298 */ 299 private static RequestWrapper wrapRequest(HttpUriRequest request) 300 throws IOException { 301 try { 302 // We have to wrap it with the right type. Some code performs 303 // instanceof checks. 304 RequestWrapper wrapped; 305 if (request instanceof HttpEntityEnclosingRequest) { 306 wrapped = new EntityEnclosingRequestWrapper( 307 (HttpEntityEnclosingRequest) request); 308 } else { 309 wrapped = new RequestWrapper(request); 310 } 311 312 // Copy the headers from the original request into the wrapper. 313 wrapped.resetHeaders(); 314 315 return wrapped; 316 } catch (ProtocolException e) { 317 throw new ClientProtocolException(e); 318 } 319 } 320 321 /** 322 * Mark a user agent as one Google will trust to handle gzipped content. 323 * {@link AndroidHttpClient#modifyRequestToAcceptGzipResponse} is (also) 324 * necessary but not sufficient -- many browsers claim to accept gzip but 325 * have broken handling, so Google checks the user agent as well. 326 * 327 * @param originalUserAgent to modify (however you identify yourself) 328 * @return user agent with a "yes, I really can handle gzip" token added. 329 * @deprecated Use {@link #GoogleHttpClient(android.content.ContentResolver, String, boolean)} 330 */ 331 public static String getGzipCapableUserAgent(String originalUserAgent) { 332 return originalUserAgent + "; gzip"; 333 } 334 335 // HttpClient wrapper methods. 336 337 public HttpParams getParams() { 338 return mClient.getParams(); 339 } 340 341 public ClientConnectionManager getConnectionManager() { 342 return mClient.getConnectionManager(); 343 } 344 345 public HttpResponse execute(HttpUriRequest request) throws IOException { 346 return execute(request, (HttpContext) null); 347 } 348 349 public HttpResponse execute(HttpHost target, HttpRequest request) 350 throws IOException { 351 return mClient.execute(target, request); 352 } 353 354 public HttpResponse execute(HttpHost target, HttpRequest request, 355 HttpContext context) throws IOException { 356 return mClient.execute(target, request, context); 357 } 358 359 public <T> T execute(HttpUriRequest request, 360 ResponseHandler<? extends T> responseHandler) 361 throws IOException, ClientProtocolException { 362 return mClient.execute(request, responseHandler); 363 } 364 365 public <T> T execute(HttpUriRequest request, 366 ResponseHandler<? extends T> responseHandler, HttpContext context) 367 throws IOException, ClientProtocolException { 368 return mClient.execute(request, responseHandler, context); 369 } 370 371 public <T> T execute(HttpHost target, HttpRequest request, 372 ResponseHandler<? extends T> responseHandler) throws IOException, 373 ClientProtocolException { 374 return mClient.execute(target, request, responseHandler); 375 } 376 377 public <T> T execute(HttpHost target, HttpRequest request, 378 ResponseHandler<? extends T> responseHandler, HttpContext context) 379 throws IOException, ClientProtocolException { 380 return mClient.execute(target, request, responseHandler, context); 381 } 382 383 /** 384 * Enables cURL request logging for this client. 385 * 386 * @param name to log messages with 387 * @param level at which to log messages (see {@link android.util.Log}) 388 */ 389 public void enableCurlLogging(String name, int level) { 390 mClient.enableCurlLogging(name, level); 391 } 392 393 /** 394 * Disables cURL logging for this client. 395 */ 396 public void disableCurlLogging() { 397 mClient.disableCurlLogging(); 398 } 399} 400