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