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