Proxy.java revision 6f71b42bc4531618e2b050bdf9ad05a72ac2a565
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.provider.Settings;
25
26import java.net.InetAddress;
27import java.net.InetSocketAddress;
28import java.net.SocketAddress;
29import java.net.URI;
30import java.net.UnknownHostException;
31import java.util.concurrent.locks.ReadWriteLock;
32import java.util.concurrent.locks.ReentrantReadWriteLock;
33import java.util.regex.Matcher;
34import java.util.regex.Pattern;
35
36import junit.framework.Assert;
37
38import org.apache.http.HttpHost;
39
40/**
41 * A convenience class for accessing the user and default proxy
42 * settings.
43 */
44public final class Proxy {
45
46    // Set to true to enable extra debugging.
47    private static final boolean DEBUG = false;
48
49    public static final String PROXY_CHANGE_ACTION =
50        "android.intent.action.PROXY_CHANGE";
51
52    private static ReadWriteLock sProxyInfoLock = new ReentrantReadWriteLock();
53
54    private static SettingsObserver sGlobalProxyChangedObserver = null;
55
56    private static ProxySpec sGlobalProxySpec = null;
57
58    // Hostname / IP REGEX validation
59    // Matches blank input, ips, and domain names
60    private static final String NAME_IP_REGEX =
61        "[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*(\\.[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*)*";
62
63    private static final String HOSTNAME_REGEXP = "^$|^" + NAME_IP_REGEX + "$";
64
65    private static final Pattern HOSTNAME_PATTERN;
66
67    private static final String EXCLLIST_REGEXP = "$|^(.?" + NAME_IP_REGEX
68        + ")+(,(.?" + NAME_IP_REGEX + "))*$";
69
70    private static final Pattern EXCLLIST_PATTERN;
71
72    static {
73        HOSTNAME_PATTERN = Pattern.compile(HOSTNAME_REGEXP);
74        EXCLLIST_PATTERN = Pattern.compile(EXCLLIST_REGEXP);
75    }
76
77    private static class ProxySpec {
78        String[] exclusionList = null;
79        InetSocketAddress proxyAddress = null;
80        public ProxySpec() { };
81    }
82
83    private static boolean isURLInExclusionListReadLocked(String url, String[] exclusionList) {
84        if (exclusionList == null) {
85            return false;
86        }
87        Uri u = Uri.parse(url);
88        String urlDomain = u.getHost();
89        // If the domain is defined as ".android.com" or "android.com", we wish to match
90        // http://android.com as well as http://xxx.android.com , but not
91        // http://myandroid.com . This code works out the logic.
92        for (String excludedDomain : exclusionList) {
93            String dotDomain = "." + excludedDomain;
94            if (urlDomain.equals(excludedDomain)) {
95                return true;
96            }
97            if (urlDomain.endsWith(dotDomain)) {
98                return true;
99            }
100        }
101        // No match
102        return false;
103    }
104
105    private static String parseHost(String proxySpec) {
106        int i = proxySpec.indexOf(':');
107        if (i == -1) {
108            if (DEBUG) {
109                Assert.assertTrue(proxySpec.length() == 0);
110            }
111            return null;
112        }
113        return proxySpec.substring(0, i);
114    }
115
116    private static int parsePort(String proxySpec) {
117        int i = proxySpec.indexOf(':');
118        if (i == -1) {
119            if (DEBUG) {
120                Assert.assertTrue(proxySpec.length() == 0);
121            }
122            return -1;
123        }
124        if (DEBUG) {
125            Assert.assertTrue(i < proxySpec.length());
126        }
127        return Integer.parseInt(proxySpec.substring(i+1));
128    }
129
130    /**
131     * Return the proxy object to be used for the URL given as parameter.
132     * @param ctx A Context used to get the settings for the proxy host.
133     * @param url A URL to be accessed. Used to evaluate exclusion list.
134     * @return Proxy (java.net) object containing the host name. If the
135     *         user did not set a hostname it returns the default host.
136     *         A null value means that no host is to be used.
137     * {@hide}
138     */
139    public static final java.net.Proxy getProxy(Context ctx, String url) {
140        sProxyInfoLock.readLock().lock();
141        java.net.Proxy retval;
142        try {
143            if (sGlobalProxyChangedObserver == null) {
144                registerContentObserversReadLocked(ctx);
145                parseGlobalProxyInfoReadLocked(ctx);
146            }
147            if (sGlobalProxySpec != null) {
148                // Proxy defined - Apply exclusion rules
149                if (isURLInExclusionListReadLocked(url, sGlobalProxySpec.exclusionList)) {
150                    // Return no proxy
151                    retval = java.net.Proxy.NO_PROXY;
152                } else {
153                    retval =
154                        new java.net.Proxy(java.net.Proxy.Type.HTTP, sGlobalProxySpec.proxyAddress);
155                }
156            } else {
157                // If network is WiFi, return no proxy.
158                // Otherwise, return the Mobile Operator proxy.
159                if (!isNetworkWifi(ctx)) {
160                    retval = getDefaultProxy(url);
161                } else {
162                    retval = java.net.Proxy.NO_PROXY;
163                }
164            }
165        } finally {
166            sProxyInfoLock.readLock().unlock();
167        }
168        if ((retval != java.net.Proxy.NO_PROXY) && (isLocalHost(url))) {
169            retval = java.net.Proxy.NO_PROXY;
170        }
171        return retval;
172    }
173
174    // TODO: deprecate this function
175    /**
176     * Return the proxy host set by the user.
177     * @param ctx A Context used to get the settings for the proxy host.
178     * @return String containing the host name. If the user did not set a host
179     *         name it returns the default host. A null value means that no
180     *         host is to be used.
181     */
182    public static final String getHost(Context ctx) {
183        sProxyInfoLock.readLock().lock();
184        try {
185            if (sGlobalProxyChangedObserver == null) {
186                registerContentObserversReadLocked(ctx);
187                parseGlobalProxyInfoReadLocked(ctx);
188            }
189            if (sGlobalProxySpec != null) {
190                InetSocketAddress sa = sGlobalProxySpec.proxyAddress;
191                return sa.getHostName();
192            }
193            return getDefaultHost();
194        } finally {
195            sProxyInfoLock.readLock().unlock();
196        }
197    }
198
199    // TODO: deprecate this function
200    /**
201     * Return the proxy port set by the user.
202     * @param ctx A Context used to get the settings for the proxy port.
203     * @return The port number to use or -1 if no proxy is to be used.
204     */
205    public static final int getPort(Context ctx) {
206        sProxyInfoLock.readLock().lock();
207        try {
208            if (sGlobalProxyChangedObserver == null) {
209                registerContentObserversReadLocked(ctx);
210                parseGlobalProxyInfoReadLocked(ctx);
211            }
212            if (sGlobalProxySpec != null) {
213                InetSocketAddress sa = sGlobalProxySpec.proxyAddress;
214                return sa.getPort();
215            }
216            return getDefaultPort();
217        } finally {
218            sProxyInfoLock.readLock().unlock();
219        }
220    }
221
222    // TODO: deprecate this function
223    /**
224     * Return the default proxy host specified by the carrier.
225     * @return String containing the host name or null if there is no proxy for
226     * this carrier.
227     */
228    public static final String getDefaultHost() {
229        String host = SystemProperties.get("net.gprs.http-proxy");
230        if ((host != null) && (host.length() != 0)) {
231            Uri u = Uri.parse(host);
232            host = u.getHost();
233            return host;
234        } else {
235            return null;
236        }
237    }
238
239    // TODO: deprecate this function
240    /**
241     * Return the default proxy port specified by the carrier.
242     * @return The port number to be used with the proxy host or -1 if there is
243     * no proxy for this carrier.
244     */
245    public static final int getDefaultPort() {
246        String host = SystemProperties.get("net.gprs.http-proxy");
247        if ((host != null) && (host.length() != 0)) {
248            Uri u = Uri.parse(host);
249            return u.getPort();
250        } else {
251            return -1;
252        }
253    }
254
255    private static final java.net.Proxy getDefaultProxy(String url) {
256        // TODO: This will go away when information is collected from ConnectivityManager...
257        // There are broadcast of network proxies, so they are parse manually.
258        String host = SystemProperties.get("net.gprs.http-proxy");
259        if ((host != null) && (host.length() != 0)) {
260            Uri u = Uri.parse(host);
261            return new java.net.Proxy(java.net.Proxy.Type.HTTP,
262                    new InetSocketAddress(u.getHost(), u.getPort()));
263        } else {
264            return java.net.Proxy.NO_PROXY;
265        }
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                if (InetAddress.getByName(host).isLoopbackAddress()) {
306                    return true;
307                }
308            }
309        } catch (UnknownHostException uex) {
310            // Ignore (INetworkSystem.ipStringToByteArray)
311        } catch (IllegalArgumentException iex) {
312            // Ignore (URI.create)
313        }
314        return false;
315    }
316
317    private static final boolean isNetworkWifi(Context context) {
318        if (context == null) {
319            return false;
320        }
321        final ConnectivityManager connectivity = (ConnectivityManager)
322            context.getSystemService(Context.CONNECTIVITY_SERVICE);
323        if (connectivity != null) {
324            final NetworkInfo info = connectivity.getActiveNetworkInfo();
325            if (info != null &&
326                    info.getType() == ConnectivityManager.TYPE_WIFI) {
327                return true;
328            }
329        }
330        return false;
331    }
332
333    private static class SettingsObserver extends ContentObserver {
334
335        private Context mContext;
336
337        SettingsObserver(Context ctx) {
338            super(new Handler(ctx.getMainLooper()));
339            mContext = ctx;
340        }
341
342        @Override
343        public void onChange(boolean selfChange) {
344            sProxyInfoLock.readLock().lock();
345            parseGlobalProxyInfoReadLocked(mContext);
346            sProxyInfoLock.readLock().unlock();
347        }
348    }
349
350    private static final void registerContentObserversReadLocked(Context ctx) {
351        Uri uriGlobalProxy = Settings.Secure.getUriFor(Settings.Secure.HTTP_PROXY);
352        Uri uriGlobalExclList =
353            Settings.Secure.getUriFor(Settings.Secure.HTTP_PROXY_EXCLUSION_LIST);
354
355        // No lock upgrading (from read to write) allowed
356        sProxyInfoLock.readLock().unlock();
357        sProxyInfoLock.writeLock().lock();
358        try {
359            sGlobalProxyChangedObserver = new SettingsObserver(ctx);
360        } finally {
361            // Downgrading locks (from write to read) is allowed
362            sProxyInfoLock.readLock().lock();
363            sProxyInfoLock.writeLock().unlock();
364        }
365        ctx.getContentResolver().registerContentObserver(uriGlobalProxy, false,
366                sGlobalProxyChangedObserver);
367        ctx.getContentResolver().registerContentObserver(uriGlobalExclList, false,
368                sGlobalProxyChangedObserver);
369    }
370
371    private static final void parseGlobalProxyInfoReadLocked(Context ctx) {
372        ContentResolver contentResolver = ctx.getContentResolver();
373        String proxyHost =  Settings.Secure.getString(
374                contentResolver,
375                Settings.Secure.HTTP_PROXY);
376        if ((proxyHost == null) || (proxyHost.length() == 0)) {
377            // Clear signal
378            sProxyInfoLock.readLock().unlock();
379            sProxyInfoLock.writeLock().lock();
380            sGlobalProxySpec = null;
381            sProxyInfoLock.readLock().lock();
382            sProxyInfoLock.writeLock().unlock();
383            return;
384        }
385        String exclusionListSpec = Settings.Secure.getString(
386                contentResolver,
387                Settings.Secure.HTTP_PROXY_EXCLUSION_LIST);
388        String host = parseHost(proxyHost);
389        int port = parsePort(proxyHost);
390        if (proxyHost != null) {
391            sGlobalProxySpec = new ProxySpec();
392            sGlobalProxySpec.proxyAddress = new InetSocketAddress(host, port);
393            if ((exclusionListSpec != null) && (exclusionListSpec.length() != 0)) {
394                String[] exclusionListEntries = exclusionListSpec.toLowerCase().split(",");
395                String[] processedEntries = new String[exclusionListEntries.length];
396                for (int i = 0; i < exclusionListEntries.length; i++) {
397                    String entry = exclusionListEntries[i].trim();
398                    if (entry.startsWith(".")) {
399                        entry = entry.substring(1);
400                    }
401                    processedEntries[i] = entry;
402                }
403                sProxyInfoLock.readLock().unlock();
404                sProxyInfoLock.writeLock().lock();
405                sGlobalProxySpec.exclusionList = processedEntries;
406            } else {
407                sProxyInfoLock.readLock().unlock();
408                sProxyInfoLock.writeLock().lock();
409                sGlobalProxySpec.exclusionList = null;
410            }
411        } else {
412            sProxyInfoLock.readLock().unlock();
413            sProxyInfoLock.writeLock().lock();
414            sGlobalProxySpec = null;
415        }
416        sProxyInfoLock.readLock().lock();
417        sProxyInfoLock.writeLock().unlock();
418    }
419
420    /**
421     * Validate syntax of hostname, port and exclusion list entries
422     * {@hide}
423     */
424    public static void validate(String hostname, String port, String exclList) {
425        Matcher match = HOSTNAME_PATTERN.matcher(hostname);
426        Matcher listMatch = EXCLLIST_PATTERN.matcher(exclList);
427
428        if (!match.matches()) {
429            throw new IllegalArgumentException();
430        }
431
432        if (!listMatch.matches()) {
433            throw new IllegalArgumentException();
434        }
435
436        if (hostname.length() > 0 && port.length() == 0) {
437            throw new IllegalArgumentException();
438        }
439
440        if (port.length() > 0) {
441            if (hostname.length() == 0) {
442                throw new IllegalArgumentException();
443            }
444            int portVal = -1;
445            try {
446                portVal = Integer.parseInt(port);
447            } catch (NumberFormatException ex) {
448                throw new IllegalArgumentException();
449            }
450            if (portVal <= 0 || portVal > 0xFFFF) {
451                throw new IllegalArgumentException();
452            }
453        }
454    }
455}
456