1 /** 2 * Copyright (c) 2018 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 17 package com.android.server.connectivity; 18 19 import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_EXCLUSION_LIST; 20 import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_HOST; 21 import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_PAC; 22 import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_PORT; 23 import static android.provider.Settings.Global.HTTP_PROXY; 24 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.content.ContentResolver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.net.LinkProperties; 31 import android.net.Network; 32 import android.net.PacProxyManager; 33 import android.net.Proxy; 34 import android.net.ProxyInfo; 35 import android.net.Uri; 36 import android.os.Binder; 37 import android.os.Handler; 38 import android.os.UserHandle; 39 import android.provider.Settings; 40 import android.text.TextUtils; 41 import android.util.Log; 42 import android.util.Pair; 43 44 import com.android.internal.annotations.GuardedBy; 45 import com.android.net.module.util.ProxyUtils; 46 47 import java.util.Collections; 48 import java.util.Objects; 49 50 /** 51 * A class to handle proxy for ConnectivityService. 52 * 53 * @hide 54 */ 55 public class ProxyTracker { 56 private static final String TAG = ProxyTracker.class.getSimpleName(); 57 private static final boolean DBG = true; 58 59 // EXTRA_PROXY_INFO is now @removed. In order to continue sending it, hardcode its value here. 60 // The Proxy.EXTRA_PROXY_INFO constant is not visible to this code because android.net.Proxy 61 // a hidden platform constant not visible to mainline modules. 62 private static final String EXTRA_PROXY_INFO = "android.intent.extra.PROXY_INFO"; 63 64 @NonNull 65 private final Context mContext; 66 67 @NonNull 68 private final Object mProxyLock = new Object(); 69 // The global proxy is the proxy that is set device-wide, overriding any network-specific 70 // proxy. Note however that proxies are hints ; the system does not enforce their use. Hence 71 // this value is only for querying. 72 @Nullable 73 @GuardedBy("mProxyLock") 74 private ProxyInfo mGlobalProxy = null; 75 // The default proxy is the proxy that applies to no particular network if the global proxy 76 // is not set. Individual networks have their own settings that override this. This member 77 // is set through setDefaultProxy, which is called when the default network changes proxies 78 // in its LinkProperties, or when ConnectivityService switches to a new default network, or 79 // when PacProxyService resolves the proxy. 80 @Nullable 81 @GuardedBy("mProxyLock") 82 private volatile ProxyInfo mDefaultProxy = null; 83 // Whether the default proxy is enabled. 84 @GuardedBy("mProxyLock") 85 private boolean mDefaultProxyEnabled = true; 86 87 private final Handler mConnectivityServiceHandler; 88 89 private final PacProxyManager mPacProxyManager; 90 91 private class PacProxyInstalledListener implements PacProxyManager.PacProxyInstalledListener { 92 private final int mEvent; 93 PacProxyInstalledListener(int event)94 PacProxyInstalledListener(int event) { 95 mEvent = event; 96 } 97 onPacProxyInstalled(@ullable Network network, @NonNull ProxyInfo proxy)98 public void onPacProxyInstalled(@Nullable Network network, @NonNull ProxyInfo proxy) { 99 Log.i(TAG, "PAC proxy installed on network " + network + " : " + proxy); 100 mConnectivityServiceHandler 101 .sendMessage(mConnectivityServiceHandler 102 .obtainMessage(mEvent, new Pair<>(network, proxy))); 103 } 104 } 105 ProxyTracker(@onNull final Context context, @NonNull final Handler connectivityServiceInternalHandler, final int pacChangedEvent)106 public ProxyTracker(@NonNull final Context context, 107 @NonNull final Handler connectivityServiceInternalHandler, final int pacChangedEvent) { 108 mContext = context; 109 mConnectivityServiceHandler = connectivityServiceInternalHandler; 110 mPacProxyManager = context.getSystemService(PacProxyManager.class); 111 112 PacProxyInstalledListener listener = new PacProxyInstalledListener(pacChangedEvent); 113 mPacProxyManager.addPacProxyInstalledListener( 114 mConnectivityServiceHandler::post, listener); 115 } 116 117 // Convert empty ProxyInfo's to null as null-checks are used to determine if proxies are present 118 // (e.g. if mGlobalProxy==null fall back to network-specific proxy, if network-specific 119 // proxy is null then there is no proxy in place). 120 @Nullable canonicalizeProxyInfo(@ullable final ProxyInfo proxy)121 private static ProxyInfo canonicalizeProxyInfo(@Nullable final ProxyInfo proxy) { 122 if (proxy != null && TextUtils.isEmpty(proxy.getHost()) 123 && Uri.EMPTY.equals(proxy.getPacFileUrl())) { 124 return null; 125 } 126 return proxy; 127 } 128 129 // ProxyInfo equality functions with a couple modifications over ProxyInfo.equals() to make it 130 // better for determining if a new proxy broadcast is necessary: 131 // 1. Canonicalize empty ProxyInfos to null so an empty proxy compares equal to null so as to 132 // avoid unnecessary broadcasts. 133 // 2. Make sure all parts of the ProxyInfo's compare true, including the host when a PAC URL 134 // is in place. This is important so legacy PAC resolver (see com.android.proxyhandler) 135 // changes aren't missed. The legacy PAC resolver pretends to be a simple HTTP proxy but 136 // actually uses the PAC to resolve; this results in ProxyInfo's with PAC URL, host and port 137 // all set. proxyInfoEqual(@ullable final ProxyInfo a, @Nullable final ProxyInfo b)138 public static boolean proxyInfoEqual(@Nullable final ProxyInfo a, @Nullable final ProxyInfo b) { 139 final ProxyInfo pa = canonicalizeProxyInfo(a); 140 final ProxyInfo pb = canonicalizeProxyInfo(b); 141 // ProxyInfo.equals() doesn't check hosts when PAC URLs are present, but we need to check 142 // hosts even when PAC URLs are present to account for the legacy PAC resolver. 143 return Objects.equals(pa, pb) && (pa == null || Objects.equals(pa.getHost(), pb.getHost())); 144 } 145 146 /** 147 * Gets the default system-wide proxy. 148 * 149 * This will return the global proxy if set, otherwise the default proxy if in use. Note 150 * that this is not necessarily the proxy that any given process should use, as the right 151 * proxy for a process is the proxy for the network this process will use, which may be 152 * different from this value. This value is simply the default in case there is no proxy set 153 * in the network that will be used by a specific process. 154 * @return The default system-wide proxy or null if none. 155 */ 156 @Nullable getDefaultProxy()157 public ProxyInfo getDefaultProxy() { 158 // This information is already available as a world read/writable jvm property. 159 synchronized (mProxyLock) { 160 if (mGlobalProxy != null) return mGlobalProxy; 161 if (mDefaultProxyEnabled) return mDefaultProxy; 162 return null; 163 } 164 } 165 166 /** 167 * Gets the global proxy. 168 * 169 * @return The global proxy or null if none. 170 */ 171 @Nullable getGlobalProxy()172 public ProxyInfo getGlobalProxy() { 173 // This information is already available as a world read/writable jvm property. 174 synchronized (mProxyLock) { 175 return mGlobalProxy; 176 } 177 } 178 179 /** 180 * Read the global proxy settings and cache them in memory. 181 */ loadGlobalProxy()182 public void loadGlobalProxy() { 183 if (loadDeprecatedGlobalHttpProxy()) { 184 return; 185 } 186 ContentResolver res = mContext.getContentResolver(); 187 String host = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_HOST); 188 int port = Settings.Global.getInt(res, GLOBAL_HTTP_PROXY_PORT, 0); 189 String exclList = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_EXCLUSION_LIST); 190 String pacFileUrl = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_PAC); 191 if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(pacFileUrl)) { 192 ProxyInfo proxyProperties; 193 if (!TextUtils.isEmpty(pacFileUrl)) { 194 proxyProperties = ProxyInfo.buildPacProxy(Uri.parse(pacFileUrl)); 195 } else { 196 proxyProperties = ProxyInfo.buildDirectProxy(host, port, 197 ProxyUtils.exclusionStringAsList(exclList)); 198 } 199 if (!proxyProperties.isValid()) { 200 if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyProperties); 201 return; 202 } 203 204 synchronized (mProxyLock) { 205 mGlobalProxy = proxyProperties; 206 } 207 208 if (!TextUtils.isEmpty(pacFileUrl)) { 209 mConnectivityServiceHandler.post( 210 () -> mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties)); 211 } 212 } 213 } 214 215 /** 216 * Read the global proxy from the deprecated Settings.Global.HTTP_PROXY setting and apply it. 217 * Returns {@code true} when global proxy was set successfully from deprecated setting. 218 */ loadDeprecatedGlobalHttpProxy()219 public boolean loadDeprecatedGlobalHttpProxy() { 220 final String proxy = Settings.Global.getString(mContext.getContentResolver(), HTTP_PROXY); 221 if (!TextUtils.isEmpty(proxy)) { 222 String data[] = proxy.split(":"); 223 if (data.length == 0) { 224 return false; 225 } 226 227 final String proxyHost = data[0]; 228 int proxyPort = 8080; 229 if (data.length > 1) { 230 try { 231 proxyPort = Integer.parseInt(data[1]); 232 } catch (NumberFormatException e) { 233 return false; 234 } 235 } 236 final ProxyInfo p = ProxyInfo.buildDirectProxy(proxyHost, proxyPort, 237 Collections.emptyList()); 238 setGlobalProxy(p); 239 return true; 240 } 241 return false; 242 } 243 244 /** 245 * Sends the system broadcast informing apps about a new proxy configuration. 246 * 247 * Confusingly this method also sets the PAC file URL. TODO : separate this, it has nothing 248 * to do in a "sendProxyBroadcast" method. 249 */ sendProxyBroadcast()250 public void sendProxyBroadcast() { 251 final ProxyInfo defaultProxy = getDefaultProxy(); 252 final ProxyInfo proxyInfo = null != defaultProxy ? 253 defaultProxy : ProxyInfo.buildDirectProxy("", 0, Collections.emptyList()); 254 mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo); 255 256 if (!shouldSendBroadcast(proxyInfo)) { 257 return; 258 } 259 if (DBG) Log.d(TAG, "sending Proxy Broadcast for " + proxyInfo); 260 Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION); 261 intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING | 262 Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); 263 intent.putExtra(EXTRA_PROXY_INFO, proxyInfo); 264 final long ident = Binder.clearCallingIdentity(); 265 try { 266 mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); 267 } finally { 268 Binder.restoreCallingIdentity(ident); 269 } 270 } 271 shouldSendBroadcast(ProxyInfo proxy)272 private boolean shouldSendBroadcast(ProxyInfo proxy) { 273 return Uri.EMPTY.equals(proxy.getPacFileUrl()) || proxy.getPort() > 0; 274 } 275 276 /** 277 * Sets the global proxy in memory. Also writes the values to the global settings of the device. 278 * 279 * @param proxyInfo the proxy spec, or null for no proxy. 280 */ setGlobalProxy(@ullable ProxyInfo proxyInfo)281 public void setGlobalProxy(@Nullable ProxyInfo proxyInfo) { 282 synchronized (mProxyLock) { 283 // ProxyInfo#equals is not commutative :( and is public API, so it can't be fixed. 284 if (proxyInfo == mGlobalProxy) return; 285 if (proxyInfo != null && proxyInfo.equals(mGlobalProxy)) return; 286 if (mGlobalProxy != null && mGlobalProxy.equals(proxyInfo)) return; 287 288 final String host; 289 final int port; 290 final String exclList; 291 final String pacFileUrl; 292 if (proxyInfo != null && (!TextUtils.isEmpty(proxyInfo.getHost()) || 293 !Uri.EMPTY.equals(proxyInfo.getPacFileUrl()))) { 294 if (!proxyInfo.isValid()) { 295 if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo); 296 return; 297 } 298 mGlobalProxy = new ProxyInfo(proxyInfo); 299 host = mGlobalProxy.getHost(); 300 port = mGlobalProxy.getPort(); 301 exclList = ProxyUtils.exclusionListAsString(mGlobalProxy.getExclusionList()); 302 pacFileUrl = Uri.EMPTY.equals(proxyInfo.getPacFileUrl()) 303 ? "" : proxyInfo.getPacFileUrl().toString(); 304 } else { 305 host = ""; 306 port = 0; 307 exclList = ""; 308 pacFileUrl = ""; 309 mGlobalProxy = null; 310 } 311 final ContentResolver res = mContext.getContentResolver(); 312 final long token = Binder.clearCallingIdentity(); 313 try { 314 Settings.Global.putString(res, GLOBAL_HTTP_PROXY_HOST, host); 315 Settings.Global.putInt(res, GLOBAL_HTTP_PROXY_PORT, port); 316 Settings.Global.putString(res, GLOBAL_HTTP_PROXY_EXCLUSION_LIST, exclList); 317 Settings.Global.putString(res, GLOBAL_HTTP_PROXY_PAC, pacFileUrl); 318 } finally { 319 Binder.restoreCallingIdentity(token); 320 } 321 322 sendProxyBroadcast(); 323 } 324 } 325 326 /** 327 * Sets the default proxy for the device. 328 * 329 * The default proxy is the proxy used for networks that do not have a specific proxy. 330 * @param proxyInfo the proxy spec, or null for no proxy. 331 */ setDefaultProxy(@ullable ProxyInfo proxyInfo)332 public void setDefaultProxy(@Nullable ProxyInfo proxyInfo) { 333 // The code has been accepting empty proxy objects forever, so for backward 334 // compatibility it should continue doing so. 335 if (proxyInfo != null && TextUtils.isEmpty(proxyInfo.getHost()) 336 && Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) { 337 proxyInfo = null; 338 } 339 synchronized (mProxyLock) { 340 if (Objects.equals(mDefaultProxy, proxyInfo)) return; 341 if (proxyInfo != null && !proxyInfo.isValid()) { 342 if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo); 343 return; 344 } 345 346 // This call could be coming from the PacProxyService, containing the port of the 347 // local proxy. If this new proxy matches the global proxy then copy this proxy to the 348 // global (to get the correct local port), and send a broadcast. 349 // TODO: Switch PacProxyService to have its own message to send back rather than 350 // reusing EVENT_HAS_CHANGED_PROXY and this call to handleApplyDefaultProxy. 351 if ((mGlobalProxy != null) && (proxyInfo != null) 352 && (!Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) 353 && proxyInfo.getPacFileUrl().equals(mGlobalProxy.getPacFileUrl())) { 354 mGlobalProxy = proxyInfo; 355 sendProxyBroadcast(); 356 return; 357 } 358 mDefaultProxy = proxyInfo; 359 360 if (mGlobalProxy != null) return; 361 if (mDefaultProxyEnabled) { 362 sendProxyBroadcast(); 363 } 364 } 365 } 366 isPacProxy(@ullable final ProxyInfo info)367 private boolean isPacProxy(@Nullable final ProxyInfo info) { 368 return null != info && info.isPacProxy(); 369 } 370 371 /** 372 * Adjust the proxy in the link properties if necessary. 373 * 374 * It is necessary when the proxy in the passed property is for PAC, and the default proxy 375 * is also for PAC. This is because the original LinkProperties from the network agent don't 376 * include the port for the local proxy as it's not known at creation time, but this class 377 * knows it after the proxy service is started. 378 * 379 * This is safe because there can only ever be one proxy service running on the device, so 380 * if the ProxyInfo in the LinkProperties is for PAC, then the port is necessarily the one 381 * ProxyTracker knows about. 382 * 383 * @param lp the LinkProperties to fix up. 384 * @param network the network of the local proxy server. 385 */ 386 // TODO: Leave network unused to support local proxy server per network in the future. updateDefaultNetworkProxyPortForPAC(@onNull final LinkProperties lp, @Nullable Network network)387 public void updateDefaultNetworkProxyPortForPAC(@NonNull final LinkProperties lp, 388 @Nullable Network network) { 389 final ProxyInfo defaultProxy = getDefaultProxy(); 390 if (isPacProxy(lp.getHttpProxy()) && isPacProxy(defaultProxy)) { 391 synchronized (mProxyLock) { 392 // At this time, this method can only be called for the default network's LP. 393 // Therefore the PAC file URL in the LP must match the one in the default proxy, 394 // and we just update the port. 395 // Note that the global proxy, if any, is set out of band by the DPM and becomes 396 // the default proxy (it overrides it, see {@link getDefaultProxy}). The PAC URL 397 // in the global proxy might not be the one in the LP of the default 398 // network, so discount this case. 399 if (null == mGlobalProxy && !lp.getHttpProxy().getPacFileUrl() 400 .equals(defaultProxy.getPacFileUrl())) { 401 throw new IllegalStateException("Unexpected discrepancy between proxy in LP of " 402 + "default network and default proxy. The former has a PAC URL of " 403 + lp.getHttpProxy().getPacFileUrl() + " while the latter has " 404 + defaultProxy.getPacFileUrl()); 405 } 406 } 407 // If this network has a PAC proxy and proxy tracker already knows about 408 // it, now is the right time to patch it in. If proxy tracker does not know 409 // about it yet, then it will be patched in when it learns about it. 410 lp.setHttpProxy(defaultProxy); 411 } 412 } 413 } 414