Proxy.java revision 6b7af6055f25022361beb2c169d2c1835922dc32
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;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.database.ContentObserver;
22import android.os.Handler;
23import android.os.SystemProperties;
24import android.text.TextUtils;
25import android.provider.Settings;
26import android.util.Log;
27
28import java.net.InetAddress;
29import java.net.InetSocketAddress;
30import java.net.ProxySelector;
31import java.net.SocketAddress;
32import java.net.URI;
33import java.net.UnknownHostException;
34import java.util.concurrent.locks.ReadWriteLock;
35import java.util.concurrent.locks.ReentrantReadWriteLock;
36import java.util.List;
37import java.util.regex.Matcher;
38import java.util.regex.Pattern;
39
40import junit.framework.Assert;
41
42import org.apache.http.conn.routing.HttpRoute;
43import org.apache.http.conn.routing.HttpRoutePlanner;
44import org.apache.http.conn.scheme.SchemeRegistry;
45import org.apache.http.HttpHost;
46import org.apache.http.HttpRequest;
47import org.apache.http.impl.conn.ProxySelectorRoutePlanner;
48import org.apache.http.protocol.HttpContext;
49
50/**
51 * A convenience class for accessing the user and default proxy
52 * settings.
53 */
54public final class Proxy {
55
56    // Set to true to enable extra debugging.
57    private static final boolean DEBUG = false;
58
59    // Used to notify an app that's caching the default connection proxy
60    // that either the default connection or its proxy has changed
61    public static final String PROXY_CHANGE_ACTION =
62        "android.intent.action.PROXY_CHANGE";
63
64    private static ReadWriteLock sProxyInfoLock = new ReentrantReadWriteLock();
65
66    private static SettingsObserver sGlobalProxyChangedObserver = null;
67
68    private static ProxySpec sGlobalProxySpec = null;
69
70    private static ConnectivityManager sConnectivityManager = null;
71
72    // Hostname / IP REGEX validation
73    // Matches blank input, ips, and domain names
74    private static final String NAME_IP_REGEX =
75        "[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*(\\.[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*)*";
76
77    private static final String HOSTNAME_REGEXP = "^$|^" + NAME_IP_REGEX + "$";
78
79    private static final Pattern HOSTNAME_PATTERN;
80
81    private static final String EXCLLIST_REGEXP = "$|^(.?" + NAME_IP_REGEX
82        + ")+(,(.?" + NAME_IP_REGEX + "))*$";
83
84    private static final Pattern EXCLLIST_PATTERN;
85
86    static {
87        HOSTNAME_PATTERN = Pattern.compile(HOSTNAME_REGEXP);
88        EXCLLIST_PATTERN = Pattern.compile(EXCLLIST_REGEXP);
89    }
90
91    // useful because it holds the processed exclusion list - don't want to reparse it each time
92    private static class ProxySpec {
93        String[] exclusionList;
94        InetSocketAddress address = null;
95        public ProxySpec() {
96            exclusionList = new String[0];
97        };
98    }
99
100    private static boolean isURLInExclusionList(String url, String[] exclusionList) {
101        if (url == null) {
102            return false;
103        }
104        Uri u = Uri.parse(url);
105        String urlDomain = u.getHost();
106        // If the domain is defined as ".android.com" or "android.com", we wish to match
107        // http://android.com as well as http://xxx.android.com , but not
108        // http://myandroid.com . This code works out the logic.
109        for (String excludedDomain : exclusionList) {
110            String dotDomain = "." + excludedDomain;
111            if (urlDomain.equals(excludedDomain)) {
112                return true;
113            }
114            if (urlDomain.endsWith(dotDomain)) {
115                return true;
116            }
117        }
118        // No match
119        return false;
120    }
121
122    private static String parseHost(String proxySpec) {
123        int i = proxySpec.indexOf(':');
124        if (i == -1) {
125            if (DEBUG) {
126                Assert.assertTrue(proxySpec.length() == 0);
127            }
128            return null;
129        }
130        return proxySpec.substring(0, i);
131    }
132
133    private static int parsePort(String proxySpec) {
134        int i = proxySpec.indexOf(':');
135        if (i == -1) {
136            if (DEBUG) {
137                Assert.assertTrue(proxySpec.length() == 0);
138            }
139            return -1;
140        }
141        if (DEBUG) {
142            Assert.assertTrue(i < proxySpec.length());
143        }
144        return Integer.parseInt(proxySpec.substring(i+1));
145    }
146
147    /**
148     * Return the proxy object to be used for the URL given as parameter.
149     * @param ctx A Context used to get the settings for the proxy host.
150     * @param url A URL to be accessed. Used to evaluate exclusion list.
151     * @return Proxy (java.net) object containing the host name. If the
152     *         user did not set a hostname it returns the default host.
153     *         A null value means that no host is to be used.
154     * {@hide}
155     */
156    public static final java.net.Proxy getProxy(Context ctx, String url) {
157        sProxyInfoLock.readLock().lock();
158        java.net.Proxy retval;
159        try {
160            if (sGlobalProxyChangedObserver == null) {
161                registerContentObserversReadLocked(ctx);
162                parseGlobalProxyInfoReadLocked(ctx);
163            }
164            if (sGlobalProxySpec != null) {
165                // Proxy defined - Apply exclusion rules
166                if (isURLInExclusionList(url, sGlobalProxySpec.exclusionList)) {
167                    // Return no proxy
168                    retval = java.net.Proxy.NO_PROXY;
169                } else {
170                    retval =
171                        new java.net.Proxy(java.net.Proxy.Type.HTTP, sGlobalProxySpec.address);
172                }
173            } else {
174                retval = getDefaultProxy(ctx, url);
175            }
176        } finally {
177            sProxyInfoLock.readLock().unlock();
178        }
179        if ((retval != java.net.Proxy.NO_PROXY) && (isLocalHost(url))) {
180            retval = java.net.Proxy.NO_PROXY;
181        }
182        return retval;
183    }
184
185    // TODO: deprecate this function
186    /**
187     * Return the proxy host set by the user.
188     * @param ctx A Context used to get the settings for the proxy host.
189     * @return String containing the host name. If the user did not set a host
190     *         name it returns the default host. A null value means that no
191     *         host is to be used.
192     */
193    public static final String getHost(Context ctx) {
194        java.net.Proxy proxy = getProxy(ctx, null);
195        if (proxy == java.net.Proxy.NO_PROXY) return null;
196        try {
197            return ((InetSocketAddress)(proxy.address())).getHostName();
198        } catch (Exception e) {
199            return null;
200        }
201    }
202
203    // TODO: deprecate this function
204    /**
205     * Return the proxy port set by the user.
206     * @param ctx A Context used to get the settings for the proxy port.
207     * @return The port number to use or -1 if no proxy is to be used.
208     */
209    public static final int getPort(Context ctx) {
210        java.net.Proxy proxy = getProxy(ctx, null);
211        if (proxy == java.net.Proxy.NO_PROXY) return -1;
212        try {
213            return ((InetSocketAddress)(proxy.address())).getPort();
214        } catch (Exception e) {
215            return -1;
216        }
217    }
218
219    // TODO: deprecate this function
220    /**
221     * Return the default proxy host specified by the carrier.
222     * @return String containing the host name or null if there is no proxy for
223     * this carrier.
224     */
225    public static final String getDefaultHost() {
226        return null;
227    }
228
229    // TODO: deprecate this function
230    /**
231     * Return the default proxy port specified by the carrier.
232     * @return The port number to be used with the proxy host or -1 if there is
233     * no proxy for this carrier.
234     */
235    public static final int getDefaultPort() {
236        return -1;
237    }
238
239    // TODO - cache the details for each network so we don't have to fetch and parse
240    // on each request
241    private static final java.net.Proxy getDefaultProxy(Context context, String url) {
242        if (sConnectivityManager == null) {
243            sConnectivityManager = (ConnectivityManager)context.getSystemService(
244                    Context.CONNECTIVITY_SERVICE);
245        }
246        if (sConnectivityManager == null) return java.net.Proxy.NO_PROXY;
247
248        LinkProperties linkProperties = sConnectivityManager.getActiveLinkProperties();
249
250        if (linkProperties != null) {
251            ProxyProperties proxyProperties = linkProperties.getHttpProxy();
252
253            if (proxyProperties != null) {
254                String exclusionList = proxyProperties.getExclusionList();
255                SocketAddress socketAddr = proxyProperties.getSocketAddress();
256                if (socketAddr != null) {
257                    String[] parsedExclusionArray =
258                            parsedExclusionArray = parseExclusionList(exclusionList);
259                    if (!isURLInExclusionList(url, parsedExclusionArray)) {
260                        return new java.net.Proxy(java.net.Proxy.Type.HTTP, socketAddr);
261                    }
262                }
263            }
264        }
265        return java.net.Proxy.NO_PROXY;
266    }
267
268    // TODO: remove this function / deprecate
269    /**
270     * Returns the preferred proxy to be used by clients. This is a wrapper
271     * around {@link android.net.Proxy#getHost()}. Currently no proxy will
272     * be returned for localhost or if the active network is Wi-Fi.
273     *
274     * @param context the context which will be passed to
275     * {@link android.net.Proxy#getHost()}
276     * @param url the target URL for the request
277     * @note Calling this method requires permission
278     * android.permission.ACCESS_NETWORK_STATE
279     * @return The preferred proxy to be used by clients, or null if there
280     * is no proxy.
281     * {@hide}
282     */
283    public static final HttpHost getPreferredHttpHost(Context context,
284            String url) {
285        java.net.Proxy prefProxy = getProxy(context, url);
286        if (prefProxy.equals(java.net.Proxy.NO_PROXY)) {
287            return null;
288        } else {
289            InetSocketAddress sa = (InetSocketAddress)prefProxy.address();
290            return new HttpHost(sa.getHostName(), sa.getPort(), "http");
291        }
292    }
293
294    private static final boolean isLocalHost(String url) {
295        if (url == null) {
296            return false;
297        }
298        try {
299            final URI uri = URI.create(url);
300            final String host = uri.getHost();
301            if (host != null) {
302                if (host.equalsIgnoreCase("localhost")) {
303                    return true;
304                }
305                // Check we have a numeric address so we don't cause a DNS lookup in getByName.
306                if (InetAddress.isNumeric(host)) {
307                    if (InetAddress.getByName(host).isLoopbackAddress()) {
308                        return true;
309                    }
310                }
311            }
312        } catch (UnknownHostException ignored) {
313            // Can't happen for a numeric address (InetAddress.getByName).
314        } catch (IllegalArgumentException iex) {
315            // Ignore (URI.create)
316        }
317        return false;
318    }
319
320    private static class SettingsObserver extends ContentObserver {
321
322        private Context mContext;
323
324        SettingsObserver(Context ctx) {
325            super(new Handler(ctx.getMainLooper()));
326            mContext = ctx;
327        }
328
329        @Override
330        public void onChange(boolean selfChange) {
331            sProxyInfoLock.readLock().lock();
332            parseGlobalProxyInfoReadLocked(mContext);
333            sProxyInfoLock.readLock().unlock();
334        }
335    }
336
337    private static final void registerContentObserversReadLocked(Context ctx) {
338        Uri uriGlobalProxy = Settings.Secure.getUriFor(Settings.Secure.HTTP_PROXY);
339        Uri uriGlobalExclList =
340            Settings.Secure.getUriFor(Settings.Secure.HTTP_PROXY_EXCLUSION_LIST);
341
342        // No lock upgrading (from read to write) allowed
343        sProxyInfoLock.readLock().unlock();
344        sProxyInfoLock.writeLock().lock();
345        try {
346            sGlobalProxyChangedObserver = new SettingsObserver(ctx);
347        } finally {
348            // Downgrading locks (from write to read) is allowed
349            sProxyInfoLock.readLock().lock();
350            sProxyInfoLock.writeLock().unlock();
351        }
352        ctx.getContentResolver().registerContentObserver(uriGlobalProxy, false,
353                sGlobalProxyChangedObserver);
354        ctx.getContentResolver().registerContentObserver(uriGlobalExclList, false,
355                sGlobalProxyChangedObserver);
356    }
357
358    private static final void parseGlobalProxyInfoReadLocked(Context ctx) {
359        ContentResolver contentResolver = ctx.getContentResolver();
360        String proxyHost =  Settings.Secure.getString(
361                contentResolver,
362                Settings.Secure.HTTP_PROXY);
363        if (TextUtils.isEmpty(proxyHost)) {
364            // Clear signal
365            sProxyInfoLock.readLock().unlock();
366            sProxyInfoLock.writeLock().lock();
367            sGlobalProxySpec = null;
368            sProxyInfoLock.readLock().lock();
369            sProxyInfoLock.writeLock().unlock();
370            return;
371        }
372        String exclusionListSpec = Settings.Secure.getString(
373                contentResolver,
374                Settings.Secure.HTTP_PROXY_EXCLUSION_LIST);
375        String host = parseHost(proxyHost);
376        int port = parsePort(proxyHost);
377        ProxySpec tmpProxySpec = null;
378        if (proxyHost != null) {
379            tmpProxySpec = new ProxySpec();
380            tmpProxySpec.address = new InetSocketAddress(host, port);
381            tmpProxySpec.exclusionList = parseExclusionList(exclusionListSpec);
382        }
383        sProxyInfoLock.readLock().unlock();
384        sProxyInfoLock.writeLock().lock();
385        sGlobalProxySpec = tmpProxySpec;
386        sProxyInfoLock.readLock().lock();
387        sProxyInfoLock.writeLock().unlock();
388    }
389
390    private static String[] parseExclusionList(String exclusionList) {
391        String[] processedArray = new String[0];
392        if (!TextUtils.isEmpty(exclusionList)) {
393            String[] exclusionListArray = exclusionList.toLowerCase().split(",");
394            processedArray = new String[exclusionListArray.length];
395            for (int i = 0; i < exclusionListArray.length; i++) {
396                String entry = exclusionListArray[i].trim();
397                if (entry.startsWith(".")) {
398                    entry = entry.substring(1);
399                }
400                processedArray[i] = entry;
401            }
402        }
403        return processedArray;
404    }
405
406    /**
407     * Validate syntax of hostname, port and exclusion list entries
408     * {@hide}
409     */
410    public static void validate(String hostname, String port, String exclList) {
411        Matcher match = HOSTNAME_PATTERN.matcher(hostname);
412        Matcher listMatch = EXCLLIST_PATTERN.matcher(exclList);
413
414        if (!match.matches()) {
415            throw new IllegalArgumentException();
416        }
417
418        if (!listMatch.matches()) {
419            throw new IllegalArgumentException();
420        }
421
422        if (hostname.length() > 0 && port.length() == 0) {
423            throw new IllegalArgumentException();
424        }
425
426        if (port.length() > 0) {
427            if (hostname.length() == 0) {
428                throw new IllegalArgumentException();
429            }
430            int portVal = -1;
431            try {
432                portVal = Integer.parseInt(port);
433            } catch (NumberFormatException ex) {
434                throw new IllegalArgumentException();
435            }
436            if (portVal <= 0 || portVal > 0xFFFF) {
437                throw new IllegalArgumentException();
438            }
439        }
440    }
441
442    static class AndroidProxySelectorRoutePlanner
443            extends org.apache.http.impl.conn.ProxySelectorRoutePlanner {
444
445        private Context mContext;
446
447        public AndroidProxySelectorRoutePlanner(SchemeRegistry schreg, ProxySelector prosel,
448                Context context) {
449            super(schreg, prosel);
450            mContext = context;
451        }
452
453        @Override
454        protected java.net.Proxy chooseProxy(List<java.net.Proxy> proxies, HttpHost target,
455                HttpRequest request, HttpContext context) {
456            return getProxy(mContext, target.getHostName());
457        }
458
459        @Override
460        protected HttpHost determineProxy(HttpHost target, HttpRequest request,
461                HttpContext context) {
462            return getPreferredHttpHost(mContext, target.getHostName());
463        }
464
465        @Override
466        public HttpRoute determineRoute(HttpHost target, HttpRequest request,
467                HttpContext context) {
468            HttpHost proxy = getPreferredHttpHost(mContext, target.getHostName());
469            if (proxy == null) {
470                return new HttpRoute(target);
471            } else {
472                return new HttpRoute(target, null, proxy, false);
473            }
474        }
475    }
476
477    /** @hide */
478    public static final HttpRoutePlanner getAndroidProxySelectorRoutePlanner(Context context) {
479        AndroidProxySelectorRoutePlanner ret = new AndroidProxySelectorRoutePlanner(
480                new SchemeRegistry(), ProxySelector.getDefault(), context);
481        return ret;
482    }
483}
484