| /* |
| * Copyright (C) 2007 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.net; |
| |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.database.ContentObserver; |
| import android.os.Handler; |
| import android.os.SystemProperties; |
| import android.text.TextUtils; |
| import android.provider.Settings; |
| import android.util.Log; |
| |
| import java.net.InetAddress; |
| import java.net.InetSocketAddress; |
| import java.net.ProxySelector; |
| import java.net.SocketAddress; |
| import java.net.URI; |
| import java.net.UnknownHostException; |
| import java.util.concurrent.locks.ReadWriteLock; |
| import java.util.concurrent.locks.ReentrantReadWriteLock; |
| import java.util.List; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import junit.framework.Assert; |
| |
| import org.apache.http.conn.routing.HttpRoute; |
| import org.apache.http.conn.routing.HttpRoutePlanner; |
| import org.apache.http.conn.scheme.SchemeRegistry; |
| import org.apache.http.HttpHost; |
| import org.apache.http.HttpRequest; |
| import org.apache.http.impl.conn.ProxySelectorRoutePlanner; |
| import org.apache.http.protocol.HttpContext; |
| |
| /** |
| * A convenience class for accessing the user and default proxy |
| * settings. |
| */ |
| public final class Proxy { |
| |
| // Set to true to enable extra debugging. |
| private static final boolean DEBUG = false; |
| |
| // Used to notify an app that's caching the default connection proxy |
| // that either the default connection or its proxy has changed |
| public static final String PROXY_CHANGE_ACTION = |
| "android.intent.action.PROXY_CHANGE"; |
| |
| private static ReadWriteLock sProxyInfoLock = new ReentrantReadWriteLock(); |
| |
| private static SettingsObserver sGlobalProxyChangedObserver = null; |
| |
| private static ProxySpec sGlobalProxySpec = null; |
| |
| private static ConnectivityManager sConnectivityManager = null; |
| |
| // Hostname / IP REGEX validation |
| // Matches blank input, ips, and domain names |
| private static final String NAME_IP_REGEX = |
| "[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*(\\.[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*)*"; |
| |
| private static final String HOSTNAME_REGEXP = "^$|^" + NAME_IP_REGEX + "$"; |
| |
| private static final Pattern HOSTNAME_PATTERN; |
| |
| private static final String EXCLLIST_REGEXP = "$|^(.?" + NAME_IP_REGEX |
| + ")+(,(.?" + NAME_IP_REGEX + "))*$"; |
| |
| private static final Pattern EXCLLIST_PATTERN; |
| |
| static { |
| HOSTNAME_PATTERN = Pattern.compile(HOSTNAME_REGEXP); |
| EXCLLIST_PATTERN = Pattern.compile(EXCLLIST_REGEXP); |
| } |
| |
| // useful because it holds the processed exclusion list - don't want to reparse it each time |
| private static class ProxySpec { |
| String[] exclusionList; |
| InetSocketAddress address = null; |
| public ProxySpec() { |
| exclusionList = new String[0]; |
| }; |
| } |
| |
| private static boolean isURLInExclusionList(String url, String[] exclusionList) { |
| if (url == null) { |
| return false; |
| } |
| Uri u = Uri.parse(url); |
| String urlDomain = u.getHost(); |
| // If the domain is defined as ".android.com" or "android.com", we wish to match |
| // http://android.com as well as http://xxx.android.com , but not |
| // http://myandroid.com . This code works out the logic. |
| for (String excludedDomain : exclusionList) { |
| String dotDomain = "." + excludedDomain; |
| if (urlDomain.equals(excludedDomain)) { |
| return true; |
| } |
| if (urlDomain.endsWith(dotDomain)) { |
| return true; |
| } |
| } |
| // No match |
| return false; |
| } |
| |
| private static String parseHost(String proxySpec) { |
| int i = proxySpec.indexOf(':'); |
| if (i == -1) { |
| if (DEBUG) { |
| Assert.assertTrue(proxySpec.length() == 0); |
| } |
| return null; |
| } |
| return proxySpec.substring(0, i); |
| } |
| |
| private static int parsePort(String proxySpec) { |
| int i = proxySpec.indexOf(':'); |
| if (i == -1) { |
| if (DEBUG) { |
| Assert.assertTrue(proxySpec.length() == 0); |
| } |
| return -1; |
| } |
| if (DEBUG) { |
| Assert.assertTrue(i < proxySpec.length()); |
| } |
| return Integer.parseInt(proxySpec.substring(i+1)); |
| } |
| |
| /** |
| * Return the proxy object to be used for the URL given as parameter. |
| * @param ctx A Context used to get the settings for the proxy host. |
| * @param url A URL to be accessed. Used to evaluate exclusion list. |
| * @return Proxy (java.net) object containing the host name. If the |
| * user did not set a hostname it returns the default host. |
| * A null value means that no host is to be used. |
| * {@hide} |
| */ |
| public static final java.net.Proxy getProxy(Context ctx, String url) { |
| sProxyInfoLock.readLock().lock(); |
| java.net.Proxy retval; |
| try { |
| if (sGlobalProxyChangedObserver == null) { |
| registerContentObserversReadLocked(ctx); |
| parseGlobalProxyInfoReadLocked(ctx); |
| } |
| if (sGlobalProxySpec != null) { |
| // Proxy defined - Apply exclusion rules |
| if (isURLInExclusionList(url, sGlobalProxySpec.exclusionList)) { |
| // Return no proxy |
| retval = java.net.Proxy.NO_PROXY; |
| } else { |
| retval = |
| new java.net.Proxy(java.net.Proxy.Type.HTTP, sGlobalProxySpec.address); |
| } |
| } else { |
| retval = getDefaultProxy(ctx, url); |
| } |
| } finally { |
| sProxyInfoLock.readLock().unlock(); |
| } |
| if ((retval != java.net.Proxy.NO_PROXY) && (isLocalHost(url))) { |
| retval = java.net.Proxy.NO_PROXY; |
| } |
| return retval; |
| } |
| |
| // TODO: deprecate this function |
| /** |
| * Return the proxy host set by the user. |
| * @param ctx A Context used to get the settings for the proxy host. |
| * @return String containing the host name. If the user did not set a host |
| * name it returns the default host. A null value means that no |
| * host is to be used. |
| */ |
| public static final String getHost(Context ctx) { |
| java.net.Proxy proxy = getProxy(ctx, null); |
| if (proxy == java.net.Proxy.NO_PROXY) return null; |
| try { |
| return ((InetSocketAddress)(proxy.address())).getHostName(); |
| } catch (Exception e) { |
| return null; |
| } |
| } |
| |
| // TODO: deprecate this function |
| /** |
| * Return the proxy port set by the user. |
| * @param ctx A Context used to get the settings for the proxy port. |
| * @return The port number to use or -1 if no proxy is to be used. |
| */ |
| public static final int getPort(Context ctx) { |
| java.net.Proxy proxy = getProxy(ctx, null); |
| if (proxy == java.net.Proxy.NO_PROXY) return -1; |
| try { |
| return ((InetSocketAddress)(proxy.address())).getPort(); |
| } catch (Exception e) { |
| return -1; |
| } |
| } |
| |
| // TODO: deprecate this function |
| /** |
| * Return the default proxy host specified by the carrier. |
| * @return String containing the host name or null if there is no proxy for |
| * this carrier. |
| */ |
| public static final String getDefaultHost() { |
| return null; |
| } |
| |
| // TODO: deprecate this function |
| /** |
| * Return the default proxy port specified by the carrier. |
| * @return The port number to be used with the proxy host or -1 if there is |
| * no proxy for this carrier. |
| */ |
| public static final int getDefaultPort() { |
| return -1; |
| } |
| |
| // TODO - cache the details for each network so we don't have to fetch and parse |
| // on each request |
| private static final java.net.Proxy getDefaultProxy(Context context, String url) { |
| if (sConnectivityManager == null) { |
| sConnectivityManager = (ConnectivityManager)context.getSystemService( |
| Context.CONNECTIVITY_SERVICE); |
| } |
| if (sConnectivityManager == null) return java.net.Proxy.NO_PROXY; |
| |
| LinkProperties linkProperties = sConnectivityManager.getActiveLinkProperties(); |
| |
| if (linkProperties != null) { |
| ProxyProperties proxyProperties = linkProperties.getHttpProxy(); |
| |
| if (proxyProperties != null) { |
| String exclusionList = proxyProperties.getExclusionList(); |
| SocketAddress socketAddr = proxyProperties.getSocketAddress(); |
| if (socketAddr != null) { |
| String[] parsedExclusionArray = |
| parsedExclusionArray = parseExclusionList(exclusionList); |
| if (!isURLInExclusionList(url, parsedExclusionArray)) { |
| return new java.net.Proxy(java.net.Proxy.Type.HTTP, socketAddr); |
| } |
| } |
| } |
| } |
| return java.net.Proxy.NO_PROXY; |
| } |
| |
| // TODO: remove this function / deprecate |
| /** |
| * Returns the preferred proxy to be used by clients. This is a wrapper |
| * around {@link android.net.Proxy#getHost()}. Currently no proxy will |
| * be returned for localhost or if the active network is Wi-Fi. |
| * |
| * @param context the context which will be passed to |
| * {@link android.net.Proxy#getHost()} |
| * @param url the target URL for the request |
| * @note Calling this method requires permission |
| * android.permission.ACCESS_NETWORK_STATE |
| * @return The preferred proxy to be used by clients, or null if there |
| * is no proxy. |
| * {@hide} |
| */ |
| public static final HttpHost getPreferredHttpHost(Context context, |
| String url) { |
| java.net.Proxy prefProxy = getProxy(context, url); |
| if (prefProxy.equals(java.net.Proxy.NO_PROXY)) { |
| return null; |
| } else { |
| InetSocketAddress sa = (InetSocketAddress)prefProxy.address(); |
| return new HttpHost(sa.getHostName(), sa.getPort(), "http"); |
| } |
| } |
| |
| private static final boolean isLocalHost(String url) { |
| if (url == null) { |
| return false; |
| } |
| try { |
| final URI uri = URI.create(url); |
| final String host = uri.getHost(); |
| if (host != null) { |
| // TODO: InetAddress.isLoopbackAddress should be used to check |
| // for localhost. However no public factory methods exist which |
| // can be used without triggering DNS lookup if host is not localhost. |
| if (host.equalsIgnoreCase("localhost") || |
| host.equals("127.0.0.1") || |
| host.equals("[::1]")) { |
| return true; |
| } |
| } |
| } catch (IllegalArgumentException iex) { |
| // Ignore (URI.create) |
| } |
| return false; |
| } |
| |
| private static class SettingsObserver extends ContentObserver { |
| |
| private Context mContext; |
| |
| SettingsObserver(Context ctx) { |
| super(new Handler(ctx.getMainLooper())); |
| mContext = ctx; |
| } |
| |
| @Override |
| public void onChange(boolean selfChange) { |
| sProxyInfoLock.readLock().lock(); |
| parseGlobalProxyInfoReadLocked(mContext); |
| sProxyInfoLock.readLock().unlock(); |
| } |
| } |
| |
| private static final void registerContentObserversReadLocked(Context ctx) { |
| Uri uriGlobalProxy = Settings.Secure.getUriFor(Settings.Secure.HTTP_PROXY); |
| Uri uriGlobalExclList = |
| Settings.Secure.getUriFor(Settings.Secure.HTTP_PROXY_EXCLUSION_LIST); |
| |
| // No lock upgrading (from read to write) allowed |
| sProxyInfoLock.readLock().unlock(); |
| sProxyInfoLock.writeLock().lock(); |
| try { |
| sGlobalProxyChangedObserver = new SettingsObserver(ctx); |
| } finally { |
| // Downgrading locks (from write to read) is allowed |
| sProxyInfoLock.readLock().lock(); |
| sProxyInfoLock.writeLock().unlock(); |
| } |
| ctx.getContentResolver().registerContentObserver(uriGlobalProxy, false, |
| sGlobalProxyChangedObserver); |
| ctx.getContentResolver().registerContentObserver(uriGlobalExclList, false, |
| sGlobalProxyChangedObserver); |
| } |
| |
| private static final void parseGlobalProxyInfoReadLocked(Context ctx) { |
| ContentResolver contentResolver = ctx.getContentResolver(); |
| String proxyHost = Settings.Secure.getString( |
| contentResolver, |
| Settings.Secure.HTTP_PROXY); |
| if (TextUtils.isEmpty(proxyHost)) { |
| // Clear signal |
| sProxyInfoLock.readLock().unlock(); |
| sProxyInfoLock.writeLock().lock(); |
| sGlobalProxySpec = null; |
| sProxyInfoLock.readLock().lock(); |
| sProxyInfoLock.writeLock().unlock(); |
| return; |
| } |
| String exclusionListSpec = Settings.Secure.getString( |
| contentResolver, |
| Settings.Secure.HTTP_PROXY_EXCLUSION_LIST); |
| String host = parseHost(proxyHost); |
| int port = parsePort(proxyHost); |
| ProxySpec tmpProxySpec = null; |
| if (proxyHost != null) { |
| tmpProxySpec = new ProxySpec(); |
| tmpProxySpec.address = new InetSocketAddress(host, port); |
| tmpProxySpec.exclusionList = parseExclusionList(exclusionListSpec); |
| } |
| sProxyInfoLock.readLock().unlock(); |
| sProxyInfoLock.writeLock().lock(); |
| sGlobalProxySpec = tmpProxySpec; |
| sProxyInfoLock.readLock().lock(); |
| sProxyInfoLock.writeLock().unlock(); |
| } |
| |
| private static String[] parseExclusionList(String exclusionList) { |
| String[] processedArray = new String[0]; |
| if (!TextUtils.isEmpty(exclusionList)) { |
| String[] exclusionListArray = exclusionList.toLowerCase().split(","); |
| processedArray = new String[exclusionListArray.length]; |
| for (int i = 0; i < exclusionListArray.length; i++) { |
| String entry = exclusionListArray[i].trim(); |
| if (entry.startsWith(".")) { |
| entry = entry.substring(1); |
| } |
| processedArray[i] = entry; |
| } |
| } |
| return processedArray; |
| } |
| |
| /** |
| * Validate syntax of hostname, port and exclusion list entries |
| * {@hide} |
| */ |
| public static void validate(String hostname, String port, String exclList) { |
| Matcher match = HOSTNAME_PATTERN.matcher(hostname); |
| Matcher listMatch = EXCLLIST_PATTERN.matcher(exclList); |
| |
| if (!match.matches()) { |
| throw new IllegalArgumentException(); |
| } |
| |
| if (!listMatch.matches()) { |
| throw new IllegalArgumentException(); |
| } |
| |
| if (hostname.length() > 0 && port.length() == 0) { |
| throw new IllegalArgumentException(); |
| } |
| |
| if (port.length() > 0) { |
| if (hostname.length() == 0) { |
| throw new IllegalArgumentException(); |
| } |
| int portVal = -1; |
| try { |
| portVal = Integer.parseInt(port); |
| } catch (NumberFormatException ex) { |
| throw new IllegalArgumentException(); |
| } |
| if (portVal <= 0 || portVal > 0xFFFF) { |
| throw new IllegalArgumentException(); |
| } |
| } |
| } |
| |
| static class AndroidProxySelectorRoutePlanner |
| extends org.apache.http.impl.conn.ProxySelectorRoutePlanner { |
| |
| private Context mContext; |
| |
| public AndroidProxySelectorRoutePlanner(SchemeRegistry schreg, ProxySelector prosel, |
| Context context) { |
| super(schreg, prosel); |
| mContext = context; |
| } |
| |
| @Override |
| protected java.net.Proxy chooseProxy(List<java.net.Proxy> proxies, HttpHost target, |
| HttpRequest request, HttpContext context) { |
| return getProxy(mContext, target.getHostName()); |
| } |
| |
| @Override |
| protected HttpHost determineProxy(HttpHost target, HttpRequest request, |
| HttpContext context) { |
| return getPreferredHttpHost(mContext, target.getHostName()); |
| } |
| |
| @Override |
| public HttpRoute determineRoute(HttpHost target, HttpRequest request, |
| HttpContext context) { |
| HttpHost proxy = getPreferredHttpHost(mContext, target.getHostName()); |
| if (proxy == null) { |
| return new HttpRoute(target); |
| } else { |
| return new HttpRoute(target, null, proxy, false); |
| } |
| } |
| } |
| |
| /** @hide */ |
| public static final HttpRoutePlanner getAndroidProxySelectorRoutePlanner(Context context) { |
| AndroidProxySelectorRoutePlanner ret = new AndroidProxySelectorRoutePlanner( |
| new SchemeRegistry(), ProxySelector.getDefault(), context); |
| return ret; |
| } |
| } |