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.provider.Settings.Global.GLOBAL_HTTP_PROXY_EXCLUSION_LIST;
20 import static android.provider.Settings.Global.GLOBAL_HTTP_PROXY_HOST;
21 import static android.provider.Settings.Global.GLOBAL_HTTP_PROXY_PAC;
22 import static android.provider.Settings.Global.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.Proxy;
31 import android.net.ProxyInfo;
32 import android.net.Uri;
33 import android.os.Binder;
34 import android.os.Handler;
35 import android.os.UserHandle;
36 import android.provider.Settings;
37 import android.text.TextUtils;
38 import android.util.Slog;
39 
40 import com.android.internal.annotations.GuardedBy;
41 
42 import java.util.Objects;
43 
44 /**
45  * A class to handle proxy for ConnectivityService.
46  *
47  * @hide
48  */
49 public class ProxyTracker {
50     private static final String TAG = ProxyTracker.class.getSimpleName();
51     private static final boolean DBG = true;
52 
53     @NonNull
54     private final Context mContext;
55 
56     @NonNull
57     private final Object mProxyLock = new Object();
58     // The global proxy is the proxy that is set device-wide, overriding any network-specific
59     // proxy. Note however that proxies are hints ; the system does not enforce their use. Hence
60     // this value is only for querying.
61     @Nullable
62     @GuardedBy("mProxyLock")
63     private ProxyInfo mGlobalProxy = null;
64     // The default proxy is the proxy that applies to no particular network if the global proxy
65     // is not set. Individual networks have their own settings that override this. This member
66     // is set through setDefaultProxy, which is called when the default network changes proxies
67     // in its LinkProperties, or when ConnectivityService switches to a new default network, or
68     // when PacManager resolves the proxy.
69     @Nullable
70     @GuardedBy("mProxyLock")
71     private volatile ProxyInfo mDefaultProxy = null;
72     // Whether the default proxy is enabled.
73     @GuardedBy("mProxyLock")
74     private boolean mDefaultProxyEnabled = true;
75 
76     private final Handler mConnectivityServiceHandler;
77 
78     // The object responsible for Proxy Auto Configuration (PAC).
79     @NonNull
80     private final PacManager mPacManager;
81 
ProxyTracker(@onNull final Context context, @NonNull final Handler connectivityServiceInternalHandler, final int pacChangedEvent)82     public ProxyTracker(@NonNull final Context context,
83             @NonNull final Handler connectivityServiceInternalHandler, final int pacChangedEvent) {
84         mContext = context;
85         mConnectivityServiceHandler = connectivityServiceInternalHandler;
86         mPacManager = new PacManager(context, connectivityServiceInternalHandler, pacChangedEvent);
87     }
88 
89     // Convert empty ProxyInfo's to null as null-checks are used to determine if proxies are present
90     // (e.g. if mGlobalProxy==null fall back to network-specific proxy, if network-specific
91     // proxy is null then there is no proxy in place).
92     @Nullable
canonicalizeProxyInfo(@ullable final ProxyInfo proxy)93     private static ProxyInfo canonicalizeProxyInfo(@Nullable final ProxyInfo proxy) {
94         if (proxy != null && TextUtils.isEmpty(proxy.getHost())
95                 && Uri.EMPTY.equals(proxy.getPacFileUrl())) {
96             return null;
97         }
98         return proxy;
99     }
100 
101     // ProxyInfo equality functions with a couple modifications over ProxyInfo.equals() to make it
102     // better for determining if a new proxy broadcast is necessary:
103     // 1. Canonicalize empty ProxyInfos to null so an empty proxy compares equal to null so as to
104     //    avoid unnecessary broadcasts.
105     // 2. Make sure all parts of the ProxyInfo's compare true, including the host when a PAC URL
106     //    is in place.  This is important so legacy PAC resolver (see com.android.proxyhandler)
107     //    changes aren't missed.  The legacy PAC resolver pretends to be a simple HTTP proxy but
108     //    actually uses the PAC to resolve; this results in ProxyInfo's with PAC URL, host and port
109     //    all set.
proxyInfoEqual(@ullable final ProxyInfo a, @Nullable final ProxyInfo b)110     public static boolean proxyInfoEqual(@Nullable final ProxyInfo a, @Nullable final ProxyInfo b) {
111         final ProxyInfo pa = canonicalizeProxyInfo(a);
112         final ProxyInfo pb = canonicalizeProxyInfo(b);
113         // ProxyInfo.equals() doesn't check hosts when PAC URLs are present, but we need to check
114         // hosts even when PAC URLs are present to account for the legacy PAC resolver.
115         return Objects.equals(pa, pb) && (pa == null || Objects.equals(pa.getHost(), pb.getHost()));
116     }
117 
118     /**
119      * Gets the default system-wide proxy.
120      *
121      * This will return the global proxy if set, otherwise the default proxy if in use. Note
122      * that this is not necessarily the proxy that any given process should use, as the right
123      * proxy for a process is the proxy for the network this process will use, which may be
124      * different from this value. This value is simply the default in case there is no proxy set
125      * in the network that will be used by a specific process.
126      * @return The default system-wide proxy or null if none.
127      */
128     @Nullable
getDefaultProxy()129     public ProxyInfo getDefaultProxy() {
130         // This information is already available as a world read/writable jvm property.
131         synchronized (mProxyLock) {
132             if (mGlobalProxy != null) return mGlobalProxy;
133             if (mDefaultProxyEnabled) return mDefaultProxy;
134             return null;
135         }
136     }
137 
138     /**
139      * Gets the global proxy.
140      *
141      * @return The global proxy or null if none.
142      */
143     @Nullable
getGlobalProxy()144     public ProxyInfo getGlobalProxy() {
145         // This information is already available as a world read/writable jvm property.
146         synchronized (mProxyLock) {
147             return mGlobalProxy;
148         }
149     }
150 
151     /**
152      * Read the global proxy settings and cache them in memory.
153      */
loadGlobalProxy()154     public void loadGlobalProxy() {
155         if (loadDeprecatedGlobalHttpProxy()) {
156             return;
157         }
158         ContentResolver res = mContext.getContentResolver();
159         String host = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_HOST);
160         int port = Settings.Global.getInt(res, GLOBAL_HTTP_PROXY_PORT, 0);
161         String exclList = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_EXCLUSION_LIST);
162         String pacFileUrl = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_PAC);
163         if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(pacFileUrl)) {
164             ProxyInfo proxyProperties;
165             if (!TextUtils.isEmpty(pacFileUrl)) {
166                 proxyProperties = new ProxyInfo(Uri.parse(pacFileUrl));
167             } else {
168                 proxyProperties = new ProxyInfo(host, port, exclList);
169             }
170             if (!proxyProperties.isValid()) {
171                 if (DBG) Slog.d(TAG, "Invalid proxy properties, ignoring: " + proxyProperties);
172                 return;
173             }
174 
175             synchronized (mProxyLock) {
176                 mGlobalProxy = proxyProperties;
177             }
178 
179             if (!TextUtils.isEmpty(pacFileUrl)) {
180                 mConnectivityServiceHandler.post(
181                         () -> mPacManager.setCurrentProxyScriptUrl(proxyProperties));
182             }
183         }
184     }
185 
186     /**
187      * Read the global proxy from the deprecated Settings.Global.HTTP_PROXY setting and apply it.
188      * Returns {@code true} when global proxy was set successfully from deprecated setting.
189      */
loadDeprecatedGlobalHttpProxy()190     public boolean loadDeprecatedGlobalHttpProxy() {
191         final String proxy = Settings.Global.getString(mContext.getContentResolver(), HTTP_PROXY);
192         if (!TextUtils.isEmpty(proxy)) {
193             String data[] = proxy.split(":");
194             if (data.length == 0) {
195                 return false;
196             }
197 
198             final String proxyHost = data[0];
199             int proxyPort = 8080;
200             if (data.length > 1) {
201                 try {
202                     proxyPort = Integer.parseInt(data[1]);
203                 } catch (NumberFormatException e) {
204                     return false;
205                 }
206             }
207             final ProxyInfo p = new ProxyInfo(proxyHost, proxyPort, "");
208             setGlobalProxy(p);
209             return true;
210         }
211         return false;
212     }
213 
214     /**
215      * Sends the system broadcast informing apps about a new proxy configuration.
216      *
217      * Confusingly this method also sets the PAC file URL. TODO : separate this, it has nothing
218      * to do in a "sendProxyBroadcast" method.
219      */
sendProxyBroadcast()220     public void sendProxyBroadcast() {
221         final ProxyInfo defaultProxy = getDefaultProxy();
222         final ProxyInfo proxyInfo = null != defaultProxy ? defaultProxy : new ProxyInfo("", 0, "");
223         if (mPacManager.setCurrentProxyScriptUrl(proxyInfo) == PacManager.DONT_SEND_BROADCAST) {
224             return;
225         }
226         if (DBG) Slog.d(TAG, "sending Proxy Broadcast for " + proxyInfo);
227         Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
228         intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING |
229                 Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
230         intent.putExtra(Proxy.EXTRA_PROXY_INFO, proxyInfo);
231         final long ident = Binder.clearCallingIdentity();
232         try {
233             mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
234         } finally {
235             Binder.restoreCallingIdentity(ident);
236         }
237     }
238 
239     /**
240      * Sets the global proxy in memory. Also writes the values to the global settings of the device.
241      *
242      * @param proxyInfo the proxy spec, or null for no proxy.
243      */
setGlobalProxy(@ullable ProxyInfo proxyInfo)244     public void setGlobalProxy(@Nullable ProxyInfo proxyInfo) {
245         synchronized (mProxyLock) {
246             // ProxyInfo#equals is not commutative :( and is public API, so it can't be fixed.
247             if (proxyInfo == mGlobalProxy) return;
248             if (proxyInfo != null && proxyInfo.equals(mGlobalProxy)) return;
249             if (mGlobalProxy != null && mGlobalProxy.equals(proxyInfo)) return;
250 
251             final String host;
252             final int port;
253             final String exclList;
254             final String pacFileUrl;
255             if (proxyInfo != null && (!TextUtils.isEmpty(proxyInfo.getHost()) ||
256                     !Uri.EMPTY.equals(proxyInfo.getPacFileUrl()))) {
257                 if (!proxyInfo.isValid()) {
258                     if (DBG) Slog.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo);
259                     return;
260                 }
261                 mGlobalProxy = new ProxyInfo(proxyInfo);
262                 host = mGlobalProxy.getHost();
263                 port = mGlobalProxy.getPort();
264                 exclList = mGlobalProxy.getExclusionListAsString();
265                 pacFileUrl = Uri.EMPTY.equals(proxyInfo.getPacFileUrl())
266                         ? "" : proxyInfo.getPacFileUrl().toString();
267             } else {
268                 host = "";
269                 port = 0;
270                 exclList = "";
271                 pacFileUrl = "";
272                 mGlobalProxy = null;
273             }
274             final ContentResolver res = mContext.getContentResolver();
275             final long token = Binder.clearCallingIdentity();
276             try {
277                 Settings.Global.putString(res, GLOBAL_HTTP_PROXY_HOST, host);
278                 Settings.Global.putInt(res, GLOBAL_HTTP_PROXY_PORT, port);
279                 Settings.Global.putString(res, GLOBAL_HTTP_PROXY_EXCLUSION_LIST, exclList);
280                 Settings.Global.putString(res, GLOBAL_HTTP_PROXY_PAC, pacFileUrl);
281             } finally {
282                 Binder.restoreCallingIdentity(token);
283             }
284 
285             sendProxyBroadcast();
286         }
287     }
288 
289     /**
290      * Sets the default proxy for the device.
291      *
292      * The default proxy is the proxy used for networks that do not have a specific proxy.
293      * @param proxyInfo the proxy spec, or null for no proxy.
294      */
setDefaultProxy(@ullable ProxyInfo proxyInfo)295     public void setDefaultProxy(@Nullable ProxyInfo proxyInfo) {
296         synchronized (mProxyLock) {
297             if (Objects.equals(mDefaultProxy, proxyInfo)) return;
298             if (proxyInfo != null &&  !proxyInfo.isValid()) {
299                 if (DBG) Slog.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo);
300                 return;
301             }
302 
303             // This call could be coming from the PacManager, containing the port of the local
304             // proxy. If this new proxy matches the global proxy then copy this proxy to the
305             // global (to get the correct local port), and send a broadcast.
306             // TODO: Switch PacManager to have its own message to send back rather than
307             // reusing EVENT_HAS_CHANGED_PROXY and this call to handleApplyDefaultProxy.
308             if ((mGlobalProxy != null) && (proxyInfo != null)
309                     && (!Uri.EMPTY.equals(proxyInfo.getPacFileUrl()))
310                     && proxyInfo.getPacFileUrl().equals(mGlobalProxy.getPacFileUrl())) {
311                 mGlobalProxy = proxyInfo;
312                 sendProxyBroadcast();
313                 return;
314             }
315             mDefaultProxy = proxyInfo;
316 
317             if (mGlobalProxy != null) return;
318             if (mDefaultProxyEnabled) {
319                 sendProxyBroadcast();
320             }
321         }
322     }
323 }
324