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