• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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