1 // Copyright 2012 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.net; 6 7 import android.content.BroadcastReceiver; 8 import android.content.Context; 9 import android.content.Intent; 10 import android.content.IntentFilter; 11 import android.net.ConnectivityManager; 12 import android.net.Proxy; 13 import android.net.ProxyInfo; 14 import android.net.Uri; 15 import android.os.Build; 16 import android.os.Bundle; 17 import android.os.Handler; 18 import android.os.Looper; 19 import android.text.TextUtils; 20 21 import androidx.annotation.RequiresApi; 22 23 import org.chromium.base.ContextUtils; 24 import org.chromium.base.Log; 25 import org.chromium.base.TraceEvent; 26 import org.chromium.base.annotations.CalledByNative; 27 import org.chromium.base.annotations.JNINamespace; 28 import org.chromium.base.annotations.NativeClassQualifiedName; 29 import org.chromium.base.annotations.NativeMethods; 30 import org.chromium.build.BuildConfig; 31 import org.chromium.build.annotations.UsedByReflection; 32 33 import java.lang.reflect.InvocationTargetException; 34 import java.lang.reflect.Method; 35 import java.util.Locale; 36 37 /** 38 * This class partners with native ProxyConfigServiceAndroid to listen for 39 * proxy change notifications from Android. 40 * 41 * Unfortunately this is called directly via reflection in a number of WebView applications 42 * to provide a hacky way to set per-application proxy settings, so it must not be mangled by 43 * Proguard. 44 */ 45 @UsedByReflection("WebView embedders call this to override proxy settings") 46 @JNINamespace("net") 47 public class ProxyChangeListener { 48 private static final String TAG = "ProxyChangeListener"; 49 private static boolean sEnabled = true; 50 51 private final Looper mLooper; 52 private final Handler mHandler; 53 54 private long mNativePtr; 55 56 // |mProxyReceiver| handles system proxy change notifications pre-M, and also proxy change 57 // notifications triggered via reflection. When its onReceive method is called, either the 58 // intent contains the new proxy information as an extra, or it indicates that we should 59 // look up the system property values. 60 // 61 // To avoid triggering as a result of system broadcasts, it is registered with an empty intent 62 // filter on M and above. 63 private ProxyReceiver mProxyReceiver; 64 65 // On M and above we also register |mRealProxyReceiver| with a matching intent filter, to act as 66 // a trigger for fetching proxy information via ConnectionManager. 67 private BroadcastReceiver mRealProxyReceiver; 68 69 private Delegate mDelegate; 70 71 private static class ProxyConfig { ProxyConfig(String host, int port, String pacUrl, String[] exclusionList)72 public ProxyConfig(String host, int port, String pacUrl, String[] exclusionList) { 73 mHost = host; 74 mPort = port; 75 mPacUrl = pacUrl; 76 mExclusionList = exclusionList; 77 } 78 fromProxyInfo(ProxyInfo proxyInfo)79 private static ProxyConfig fromProxyInfo(ProxyInfo proxyInfo) { 80 if (proxyInfo == null) { 81 return null; 82 } 83 final String host = proxyInfo.getHost(); 84 final Uri pacFileUrl = proxyInfo.getPacFileUrl(); 85 return new ProxyConfig(host == null ? "" : host, proxyInfo.getPort(), 86 Uri.EMPTY.equals(pacFileUrl) ? null : pacFileUrl.toString(), 87 proxyInfo.getExclusionList()); 88 } 89 90 @Override toString()91 public String toString() { 92 String possiblyRedactedHost = 93 mHost.equals("localhost") || mHost.isEmpty() ? mHost : "<redacted>"; 94 return String.format(Locale.US, "ProxyConfig [mHost=\"%s\", mPort=%d, mPacUrl=%s]", 95 possiblyRedactedHost, mPort, mPacUrl == null ? "null" : "\"<redacted>\""); 96 } 97 98 public final String mHost; 99 public final int mPort; 100 public final String mPacUrl; 101 public final String[] mExclusionList; 102 103 public static final ProxyConfig DIRECT = new ProxyConfig("", 0, "", new String[0]); 104 } 105 106 /** 107 * The delegate for ProxyChangeListener. Use for testing. 108 */ proxySettingsChanged()109 public interface Delegate { public void proxySettingsChanged(); } 110 ProxyChangeListener()111 private ProxyChangeListener() { 112 mLooper = Looper.myLooper(); 113 mHandler = new Handler(mLooper); 114 } 115 setEnabled(boolean enabled)116 public static void setEnabled(boolean enabled) { 117 sEnabled = enabled; 118 } 119 setDelegateForTesting(Delegate delegate)120 public void setDelegateForTesting(Delegate delegate) { 121 mDelegate = delegate; 122 } 123 124 @CalledByNative create()125 public static ProxyChangeListener create() { 126 return new ProxyChangeListener(); 127 } 128 129 @CalledByNative getProperty(String property)130 public static String getProperty(String property) { 131 return System.getProperty(property); 132 } 133 134 @CalledByNative start(long nativePtr)135 public void start(long nativePtr) { 136 try (TraceEvent e = TraceEvent.scoped("ProxyChangeListener.start")) { 137 assertOnThread(); 138 assert mNativePtr == 0; 139 mNativePtr = nativePtr; 140 registerBroadcastReceiver(); 141 } 142 } 143 144 @CalledByNative stop()145 public void stop() { 146 assertOnThread(); 147 mNativePtr = 0; 148 unregisterBroadcastReceiver(); 149 } 150 151 @UsedByReflection("WebView embedders call this to override proxy settings") 152 private class ProxyReceiver extends BroadcastReceiver { 153 @Override 154 @UsedByReflection("WebView embedders call this to override proxy settings") onReceive(Context context, final Intent intent)155 public void onReceive(Context context, final Intent intent) { 156 if (intent.getAction().equals(Proxy.PROXY_CHANGE_ACTION)) { 157 runOnThread(() -> proxySettingsChanged(extractNewProxy(intent))); 158 } 159 } 160 } 161 162 // Extract a ProxyConfig object from the supplied Intent's extra data 163 // bundle. The android.net.ProxyProperties class is not exported from 164 // the Android SDK, so we have to use reflection to get at it and invoke 165 // methods on it. If we fail, return an empty proxy config (meaning 166 // use system properties). extractNewProxy(Intent intent)167 private static ProxyConfig extractNewProxy(Intent intent) { 168 Bundle extras = intent.getExtras(); 169 if (extras == null) { 170 return null; 171 } 172 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 173 return ProxyConfig.fromProxyInfo( 174 (ProxyInfo) extras.get("android.intent.extra.PROXY_INFO")); 175 } 176 177 try { 178 final String getHostName = "getHost"; 179 final String getPortName = "getPort"; 180 final String getPacFileUrl = "getPacFileUrl"; 181 final String getExclusionList = "getExclusionList"; 182 final String className = "android.net.ProxyProperties"; 183 184 Object props = extras.get("proxy"); 185 if (props == null) { 186 return null; 187 } 188 189 Class<?> cls = Class.forName(className); 190 Method getHostMethod = cls.getDeclaredMethod(getHostName); 191 Method getPortMethod = cls.getDeclaredMethod(getPortName); 192 Method getExclusionListMethod = cls.getDeclaredMethod(getExclusionList); 193 194 String host = (String) getHostMethod.invoke(props); 195 int port = (Integer) getPortMethod.invoke(props); 196 197 String[] exclusionList; 198 String s = (String) getExclusionListMethod.invoke(props); 199 exclusionList = s.split(","); 200 201 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 202 Method getPacFileUrlMethod = cls.getDeclaredMethod(getPacFileUrl); 203 String pacFileUrl = (String) getPacFileUrlMethod.invoke(props); 204 if (!TextUtils.isEmpty(pacFileUrl)) { 205 return new ProxyConfig(host, port, pacFileUrl, exclusionList); 206 } 207 } 208 return new ProxyConfig(host, port, null, exclusionList); 209 } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException 210 | InvocationTargetException | NullPointerException ex) { 211 Log.e(TAG, "Using no proxy configuration due to exception:" + ex); 212 return null; 213 } 214 } 215 proxySettingsChanged(ProxyConfig cfg)216 private void proxySettingsChanged(ProxyConfig cfg) { 217 assertOnThread(); 218 219 if (!sEnabled) { 220 return; 221 } 222 if (mDelegate != null) { 223 // proxySettingsChanged is called even if mNativePtr == 0, for testing purposes. 224 mDelegate.proxySettingsChanged(); 225 } 226 if (mNativePtr == 0) { 227 return; 228 } 229 230 if (cfg != null) { 231 ProxyChangeListenerJni.get().proxySettingsChangedTo(mNativePtr, 232 ProxyChangeListener.this, cfg.mHost, cfg.mPort, cfg.mPacUrl, 233 cfg.mExclusionList); 234 } else { 235 ProxyChangeListenerJni.get().proxySettingsChanged(mNativePtr, ProxyChangeListener.this); 236 } 237 } 238 239 @RequiresApi(Build.VERSION_CODES.M) getProxyConfig(Intent intent)240 private ProxyConfig getProxyConfig(Intent intent) { 241 ConnectivityManager connectivityManager = 242 (ConnectivityManager) ContextUtils.getApplicationContext().getSystemService( 243 Context.CONNECTIVITY_SERVICE); 244 ProxyConfig configFromConnectivityManager = 245 ProxyConfig.fromProxyInfo(connectivityManager.getDefaultProxy()); 246 247 if (configFromConnectivityManager == null) { 248 return ProxyConfig.DIRECT; 249 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q 250 && configFromConnectivityManager.mHost.equals("localhost") 251 && configFromConnectivityManager.mPort == -1) { 252 ProxyConfig configFromIntent = extractNewProxy(intent); 253 Log.i(TAG, "configFromConnectivityManager = %s, configFromIntent = %s", 254 configFromConnectivityManager, configFromIntent); 255 256 // There's a bug in Android Q+ PAC support. If ConnectivityManager returns localhost:-1 257 // then use the intent from the PROXY_CHANGE_ACTION broadcast to extract the 258 // ProxyConfig's host and port. See http://crbug.com/993538. 259 // 260 // -1 is never a reasonable port so just keep this workaround for future versions until 261 // we're sure it's fixed on the platform side. 262 if (configFromIntent == null) return null; 263 String correctHost = configFromIntent.mHost; 264 int correctPort = configFromIntent.mPort; 265 return new ProxyConfig(correctHost, correctPort, configFromConnectivityManager.mPacUrl, 266 configFromConnectivityManager.mExclusionList); 267 } 268 return configFromConnectivityManager; 269 } 270 updateProxyConfigFromConnectivityManager(Intent intent)271 /* package */ void updateProxyConfigFromConnectivityManager(Intent intent) { 272 runOnThread(() -> proxySettingsChanged(getProxyConfig(intent))); 273 } 274 registerBroadcastReceiver()275 private void registerBroadcastReceiver() { 276 assertOnThread(); 277 assert mProxyReceiver == null; 278 assert mRealProxyReceiver == null; 279 280 IntentFilter filter = new IntentFilter(); 281 filter.addAction(Proxy.PROXY_CHANGE_ACTION); 282 283 mProxyReceiver = new ProxyReceiver(); 284 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 285 // Proxy change broadcast receiver for Pre-M. Uses reflection to extract proxy 286 // information from the intent extra. 287 ContextUtils.registerProtectedBroadcastReceiver( 288 ContextUtils.getApplicationContext(), mProxyReceiver, filter); 289 } else { 290 if (!ContextUtils.isSdkSandboxProcess()) { 291 // Register the instance of ProxyReceiver with an empty intent filter, so that it is 292 // still found via reflection, but is not called by the system. See: 293 // crbug.com/851995 294 // 295 // Don't do this within an SDK Sandbox, because neither reflection nor registering a 296 // broadcast receiver with a blank IntentFilter is allowed. 297 ContextUtils.registerNonExportedBroadcastReceiver( 298 ContextUtils.getApplicationContext(), mProxyReceiver, new IntentFilter()); 299 } 300 301 // Create a BroadcastReceiver that uses M+ APIs to fetch the proxy confuguration from 302 // ConnectionManager. 303 mRealProxyReceiver = new ProxyBroadcastReceiver(this); 304 ContextUtils.registerProtectedBroadcastReceiver( 305 ContextUtils.getApplicationContext(), mRealProxyReceiver, filter); 306 } 307 } 308 unregisterBroadcastReceiver()309 private void unregisterBroadcastReceiver() { 310 assertOnThread(); 311 assert mProxyReceiver != null; 312 313 ContextUtils.getApplicationContext().unregisterReceiver(mProxyReceiver); 314 if (mRealProxyReceiver != null) { 315 ContextUtils.getApplicationContext().unregisterReceiver(mRealProxyReceiver); 316 } 317 mProxyReceiver = null; 318 mRealProxyReceiver = null; 319 } 320 onThread()321 private boolean onThread() { 322 return mLooper == Looper.myLooper(); 323 } 324 assertOnThread()325 private void assertOnThread() { 326 if (BuildConfig.ENABLE_ASSERTS && !onThread()) { 327 throw new IllegalStateException("Must be called on ProxyChangeListener thread."); 328 } 329 } 330 runOnThread(Runnable r)331 private void runOnThread(Runnable r) { 332 if (onThread()) { 333 r.run(); 334 } else { 335 mHandler.post(r); 336 } 337 } 338 339 /** 340 * See net/proxy_resolution/proxy_config_service_android.cc 341 */ 342 343 @NativeMethods 344 interface Natives { 345 @NativeClassQualifiedName("ProxyConfigServiceAndroid::JNIDelegate") proxySettingsChangedTo(long nativePtr, ProxyChangeListener caller, String host, int port, String pacUrl, String[] exclusionList)346 void proxySettingsChangedTo(long nativePtr, ProxyChangeListener caller, String host, 347 int port, String pacUrl, String[] exclusionList); 348 349 @NativeClassQualifiedName("ProxyConfigServiceAndroid::JNIDelegate") proxySettingsChanged(long nativePtr, ProxyChangeListener caller)350 void proxySettingsChanged(long nativePtr, ProxyChangeListener caller); 351 } 352 } 353