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