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