Proxy.java revision 16fb7910fd3a86780edf4e65b529542ae95c26cd
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        try {
142            if (sGlobalProxyChangedObserver == null) {
143                registerContentObserversReadLocked(ctx);
144                parseGlobalProxyInfoReadLocked(ctx);
145            }
146            if (sGlobalProxySpec != null) {
147                // Proxy defined - Apply exclusion rules
148                if (isURLInExclusionListReadLocked(url, sGlobalProxySpec.exclusionList)) {
149                    // Return no proxy
150                    return java.net.Proxy.NO_PROXY;
151                }
152                java.net.Proxy retProxy =
153                    new java.net.Proxy(java.net.Proxy.Type.HTTP, sGlobalProxySpec.proxyAddress);
154                sProxyInfoLock.readLock().unlock();
155                if (isLocalHost(url)) {
156                    return java.net.Proxy.NO_PROXY;
157                }
158                sProxyInfoLock.readLock().lock();
159                return retProxy;
160            } else {
161                // If network is WiFi, return no proxy.
162                // Otherwise, return the Mobile Operator proxy.
163                if (!isNetworkWifi(ctx)) {
164                    java.net.Proxy retProxy = getDefaultProxy(url);
165                    sProxyInfoLock.readLock().unlock();
166                    if (isLocalHost(url)) {
167                        return java.net.Proxy.NO_PROXY;
168                    }
169                    sProxyInfoLock.readLock().lock();
170                    return retProxy;
171                } else {
172                    return java.net.Proxy.NO_PROXY;
173                }
174            }
175        } finally {
176            sProxyInfoLock.readLock().unlock();
177        }
178    }
179
180    // TODO: deprecate this function
181    /**
182     * Return the proxy host set by the user.
183     * @param ctx A Context used to get the settings for the proxy host.
184     * @return String containing the host name. If the user did not set a host
185     *         name it returns the default host. A null value means that no
186     *         host is to be used.
187     */
188    public static final String getHost(Context ctx) {
189        sProxyInfoLock.readLock().lock();
190        try {
191            if (sGlobalProxyChangedObserver == null) {
192                registerContentObserversReadLocked(ctx);
193                parseGlobalProxyInfoReadLocked(ctx);
194            }
195            if (sGlobalProxySpec != null) {
196                InetSocketAddress sa = sGlobalProxySpec.proxyAddress;
197                return sa.getHostName();
198            }
199            return getDefaultHost();
200        } finally {
201            sProxyInfoLock.readLock().unlock();
202        }
203    }
204
205    // TODO: deprecate this function
206    /**
207     * Return the proxy port set by the user.
208     * @param ctx A Context used to get the settings for the proxy port.
209     * @return The port number to use or -1 if no proxy is to be used.
210     */
211    public static final int getPort(Context ctx) {
212        sProxyInfoLock.readLock().lock();
213        try {
214            if (sGlobalProxyChangedObserver == null) {
215                registerContentObserversReadLocked(ctx);
216                parseGlobalProxyInfoReadLocked(ctx);
217            }
218            if (sGlobalProxySpec != null) {
219                InetSocketAddress sa = sGlobalProxySpec.proxyAddress;
220                return sa.getPort();
221            }
222            return getDefaultPort();
223        } finally {
224            sProxyInfoLock.readLock().unlock();
225        }
226    }
227
228    // TODO: deprecate this function
229    /**
230     * Return the default proxy host specified by the carrier.
231     * @return String containing the host name or null if there is no proxy for
232     * this carrier.
233     */
234    public static final String getDefaultHost() {
235        String host = SystemProperties.get("net.gprs.http-proxy");
236        if (host != null) {
237            Uri u = Uri.parse(host);
238            host = u.getHost();
239            return host;
240        } else {
241            return null;
242        }
243    }
244
245    // TODO: deprecate this function
246    /**
247     * Return the default proxy port specified by the carrier.
248     * @return The port number to be used with the proxy host or -1 if there is
249     * no proxy for this carrier.
250     */
251    public static final int getDefaultPort() {
252        String host = SystemProperties.get("net.gprs.http-proxy");
253        if (host != null) {
254            Uri u = Uri.parse(host);
255            return u.getPort();
256        } else {
257            return -1;
258        }
259    }
260
261    private static final java.net.Proxy getDefaultProxy(String url) {
262        // TODO: This will go away when information is collected from ConnectivityManager...
263        // There are broadcast of network proxies, so they are parse manually.
264        String host = SystemProperties.get("net.gprs.http-proxy");
265        if (host != null) {
266            Uri u = Uri.parse(host);
267            return new java.net.Proxy(java.net.Proxy.Type.HTTP,
268                    new InetSocketAddress(u.getHost(), u.getPort()));
269        } else {
270            return java.net.Proxy.NO_PROXY;
271        }
272    }
273
274    // TODO: remove this function / deprecate
275    /**
276     * Returns the preferred proxy to be used by clients. This is a wrapper
277     * around {@link android.net.Proxy#getHost()}. Currently no proxy will
278     * be returned for localhost or if the active network is Wi-Fi.
279     *
280     * @param context the context which will be passed to
281     * {@link android.net.Proxy#getHost()}
282     * @param url the target URL for the request
283     * @note Calling this method requires permission
284     * android.permission.ACCESS_NETWORK_STATE
285     * @return The preferred proxy to be used by clients, or null if there
286     * is no proxy.
287     * {@hide}
288     */
289    public static final HttpHost getPreferredHttpHost(Context context,
290            String url) {
291        java.net.Proxy prefProxy = getProxy(context, url);
292        if (prefProxy.equals(java.net.Proxy.NO_PROXY)) {
293            return null;
294        } else {
295            InetSocketAddress sa = (InetSocketAddress)prefProxy.address();
296            return new HttpHost(sa.getHostName(), sa.getPort(), "http");
297        }
298    }
299
300    private static final boolean isLocalHost(String url) {
301        if (url == null) {
302            return false;
303        }
304        try {
305            final URI uri = URI.create(url);
306            final String host = uri.getHost();
307            if (host != null) {
308                if (host.equalsIgnoreCase("localhost")) {
309                    return true;
310                }
311                if (InetAddress.getByName(host).isLoopbackAddress()) {
312                    return true;
313                }
314            }
315        } catch (UnknownHostException uex) {
316            // Ignore (INetworkSystem.ipStringToByteArray)
317        } catch (IllegalArgumentException iex) {
318            // Ignore (URI.create)
319        }
320        return false;
321    }
322
323    private static final boolean isNetworkWifi(Context context) {
324        if (context == null) {
325            return false;
326        }
327        final ConnectivityManager connectivity = (ConnectivityManager)
328            context.getSystemService(Context.CONNECTIVITY_SERVICE);
329        if (connectivity != null) {
330            final NetworkInfo info = connectivity.getActiveNetworkInfo();
331            if (info != null &&
332                    info.getType() == ConnectivityManager.TYPE_WIFI) {
333                return true;
334            }
335        }
336        return false;
337    }
338
339    private static class SettingsObserver extends ContentObserver {
340
341        private Context mContext;
342
343        SettingsObserver(Context ctx) {
344            super(new Handler());
345            mContext = ctx;
346        }
347
348        @Override
349        public void onChange(boolean selfChange) {
350            sProxyInfoLock.readLock().lock();
351            parseGlobalProxyInfoReadLocked(mContext);
352            sProxyInfoLock.readLock().unlock();
353        }
354    }
355
356    private static final void registerContentObserversReadLocked(Context ctx) {
357        Uri uriGlobalProxy = Settings.Secure.getUriFor(Settings.Secure.HTTP_PROXY);
358        Uri uriGlobalExclList =
359            Settings.Secure.getUriFor(Settings.Secure.HTTP_PROXY_EXCLUSION_LIST);
360
361        // No lock upgrading (from read to write) allowed
362        sProxyInfoLock.readLock().unlock();
363        sProxyInfoLock.writeLock().lock();
364        sGlobalProxyChangedObserver = new SettingsObserver(ctx);
365        // Downgrading locks (from write to read) is allowed
366        sProxyInfoLock.readLock().lock();
367        sProxyInfoLock.writeLock().unlock();
368        ctx.getContentResolver().registerContentObserver(uriGlobalProxy, false,
369                sGlobalProxyChangedObserver);
370        ctx.getContentResolver().registerContentObserver(uriGlobalExclList, false,
371                sGlobalProxyChangedObserver);
372    }
373
374    private static final void parseGlobalProxyInfoReadLocked(Context ctx) {
375        ContentResolver contentResolver = ctx.getContentResolver();
376        String proxyHost =  Settings.Secure.getString(
377                contentResolver,
378                Settings.Secure.HTTP_PROXY);
379        if (proxyHost == null) {
380            return;
381        }
382        String exclusionListSpec = Settings.Secure.getString(
383                contentResolver,
384                Settings.Secure.HTTP_PROXY_EXCLUSION_LIST);
385        String host = parseHost(proxyHost);
386        int port = parsePort(proxyHost);
387        if (proxyHost != null) {
388            sGlobalProxySpec = new ProxySpec();
389            sGlobalProxySpec.proxyAddress = new InetSocketAddress(host, port);
390            if (exclusionListSpec != null) {
391                String[] exclusionListEntries = exclusionListSpec.toLowerCase().split(",");
392                String[] processedEntries = new String[exclusionListEntries.length];
393                for (int i = 0; i < exclusionListEntries.length; i++) {
394                    String entry = exclusionListEntries[i].trim();
395                    if (entry.startsWith(".")) {
396                        entry = entry.substring(1);
397                    }
398                    processedEntries[i] = entry;
399                }
400                sProxyInfoLock.readLock().unlock();
401                sProxyInfoLock.writeLock().lock();
402                sGlobalProxySpec.exclusionList = processedEntries;
403            } else {
404                sProxyInfoLock.readLock().unlock();
405                sProxyInfoLock.writeLock().lock();
406                sGlobalProxySpec.exclusionList = null;
407            }
408        } else {
409            sProxyInfoLock.readLock().unlock();
410            sProxyInfoLock.writeLock().lock();
411            sGlobalProxySpec = null;
412        }
413        sProxyInfoLock.readLock().lock();
414        sProxyInfoLock.writeLock().unlock();
415    }
416
417    /**
418     * Validate syntax of hostname, port and exclusion list entries
419     * {@hide}
420     */
421    public static void validate(String hostname, String port, String exclList) {
422        Matcher match = HOSTNAME_PATTERN.matcher(hostname);
423        Matcher listMatch = EXCLLIST_PATTERN.matcher(exclList);
424
425        if (!match.matches()) {
426            throw new IllegalArgumentException();
427        }
428
429        if (!listMatch.matches()) {
430            throw new IllegalArgumentException();
431        }
432
433        if (hostname.length() > 0 && port.length() == 0) {
434            throw new IllegalArgumentException();
435        }
436
437        if (port.length() > 0) {
438            if (hostname.length() == 0) {
439                throw new IllegalArgumentException();
440            }
441            int portVal = -1;
442            try {
443                portVal = Integer.parseInt(port);
444            } catch (NumberFormatException ex) {
445                throw new IllegalArgumentException();
446            }
447            if (portVal <= 0 || portVal > 0xFFFF) {
448                throw new IllegalArgumentException();
449            }
450        }
451    }
452}
453