• 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.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