1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
|
/**
* Copyright (c) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.connectivity;
import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_EXCLUSION_LIST;
import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_HOST;
import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_PAC;
import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_PORT;
import static android.provider.Settings.Global.HTTP_PROXY;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Network;
import android.net.PacProxyManager;
import android.net.Proxy;
import android.net.ProxyInfo;
import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import com.android.internal.annotations.GuardedBy;
import com.android.net.module.util.ProxyUtils;
import java.util.Collections;
import java.util.Objects;
/**
* A class to handle proxy for ConnectivityService.
*
* @hide
*/
public class ProxyTracker {
private static final String TAG = ProxyTracker.class.getSimpleName();
private static final boolean DBG = true;
// EXTRA_PROXY_INFO is now @removed. In order to continue sending it, hardcode its value here.
// The Proxy.EXTRA_PROXY_INFO constant is not visible to this code because android.net.Proxy
// a hidden platform constant not visible to mainline modules.
private static final String EXTRA_PROXY_INFO = "android.intent.extra.PROXY_INFO";
@NonNull
private final Context mContext;
@NonNull
private final Object mProxyLock = new Object();
// The global proxy is the proxy that is set device-wide, overriding any network-specific
// proxy. Note however that proxies are hints ; the system does not enforce their use. Hence
// this value is only for querying.
@Nullable
@GuardedBy("mProxyLock")
private ProxyInfo mGlobalProxy = null;
// The default proxy is the proxy that applies to no particular network if the global proxy
// is not set. Individual networks have their own settings that override this. This member
// is set through setDefaultProxy, which is called when the default network changes proxies
// in its LinkProperties, or when ConnectivityService switches to a new default network, or
// when PacProxyService resolves the proxy.
@Nullable
@GuardedBy("mProxyLock")
private volatile ProxyInfo mDefaultProxy = null;
// Whether the default proxy is enabled.
@GuardedBy("mProxyLock")
private boolean mDefaultProxyEnabled = true;
private final Handler mConnectivityServiceHandler;
private final PacProxyManager mPacProxyManager;
private class PacProxyInstalledListener implements PacProxyManager.PacProxyInstalledListener {
private final int mEvent;
PacProxyInstalledListener(int event) {
mEvent = event;
}
public void onPacProxyInstalled(@Nullable Network network, @NonNull ProxyInfo proxy) {
mConnectivityServiceHandler
.sendMessage(mConnectivityServiceHandler
.obtainMessage(mEvent, new Pair<>(network, proxy)));
}
}
public ProxyTracker(@NonNull final Context context,
@NonNull final Handler connectivityServiceInternalHandler, final int pacChangedEvent) {
mContext = context;
mConnectivityServiceHandler = connectivityServiceInternalHandler;
mPacProxyManager = context.getSystemService(PacProxyManager.class);
PacProxyInstalledListener listener = new PacProxyInstalledListener(pacChangedEvent);
mPacProxyManager.addPacProxyInstalledListener(
mConnectivityServiceHandler::post, listener);
}
// Convert empty ProxyInfo's to null as null-checks are used to determine if proxies are present
// (e.g. if mGlobalProxy==null fall back to network-specific proxy, if network-specific
// proxy is null then there is no proxy in place).
@Nullable
private static ProxyInfo canonicalizeProxyInfo(@Nullable final ProxyInfo proxy) {
if (proxy != null && TextUtils.isEmpty(proxy.getHost())
&& Uri.EMPTY.equals(proxy.getPacFileUrl())) {
return null;
}
return proxy;
}
// ProxyInfo equality functions with a couple modifications over ProxyInfo.equals() to make it
// better for determining if a new proxy broadcast is necessary:
// 1. Canonicalize empty ProxyInfos to null so an empty proxy compares equal to null so as to
// avoid unnecessary broadcasts.
// 2. Make sure all parts of the ProxyInfo's compare true, including the host when a PAC URL
// is in place. This is important so legacy PAC resolver (see com.android.proxyhandler)
// changes aren't missed. The legacy PAC resolver pretends to be a simple HTTP proxy but
// actually uses the PAC to resolve; this results in ProxyInfo's with PAC URL, host and port
// all set.
public static boolean proxyInfoEqual(@Nullable final ProxyInfo a, @Nullable final ProxyInfo b) {
final ProxyInfo pa = canonicalizeProxyInfo(a);
final ProxyInfo pb = canonicalizeProxyInfo(b);
// ProxyInfo.equals() doesn't check hosts when PAC URLs are present, but we need to check
// hosts even when PAC URLs are present to account for the legacy PAC resolver.
return Objects.equals(pa, pb) && (pa == null || Objects.equals(pa.getHost(), pb.getHost()));
}
/**
* Gets the default system-wide proxy.
*
* This will return the global proxy if set, otherwise the default proxy if in use. Note
* that this is not necessarily the proxy that any given process should use, as the right
* proxy for a process is the proxy for the network this process will use, which may be
* different from this value. This value is simply the default in case there is no proxy set
* in the network that will be used by a specific process.
* @return The default system-wide proxy or null if none.
*/
@Nullable
public ProxyInfo getDefaultProxy() {
// This information is already available as a world read/writable jvm property.
synchronized (mProxyLock) {
if (mGlobalProxy != null) return mGlobalProxy;
if (mDefaultProxyEnabled) return mDefaultProxy;
return null;
}
}
/**
* Gets the global proxy.
*
* @return The global proxy or null if none.
*/
@Nullable
public ProxyInfo getGlobalProxy() {
// This information is already available as a world read/writable jvm property.
synchronized (mProxyLock) {
return mGlobalProxy;
}
}
/**
* Read the global proxy settings and cache them in memory.
*/
public void loadGlobalProxy() {
if (loadDeprecatedGlobalHttpProxy()) {
return;
}
ContentResolver res = mContext.getContentResolver();
String host = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_HOST);
int port = Settings.Global.getInt(res, GLOBAL_HTTP_PROXY_PORT, 0);
String exclList = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_EXCLUSION_LIST);
String pacFileUrl = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_PAC);
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(pacFileUrl)) {
ProxyInfo proxyProperties;
if (!TextUtils.isEmpty(pacFileUrl)) {
proxyProperties = ProxyInfo.buildPacProxy(Uri.parse(pacFileUrl));
} else {
proxyProperties = ProxyInfo.buildDirectProxy(host, port,
ProxyUtils.exclusionStringAsList(exclList));
}
if (!proxyProperties.isValid()) {
if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyProperties);
return;
}
synchronized (mProxyLock) {
mGlobalProxy = proxyProperties;
}
if (!TextUtils.isEmpty(pacFileUrl)) {
mConnectivityServiceHandler.post(
() -> mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties));
}
}
}
/**
* Read the global proxy from the deprecated Settings.Global.HTTP_PROXY setting and apply it.
* Returns {@code true} when global proxy was set successfully from deprecated setting.
*/
public boolean loadDeprecatedGlobalHttpProxy() {
final String proxy = Settings.Global.getString(mContext.getContentResolver(), HTTP_PROXY);
if (!TextUtils.isEmpty(proxy)) {
String data[] = proxy.split(":");
if (data.length == 0) {
return false;
}
final String proxyHost = data[0];
int proxyPort = 8080;
if (data.length > 1) {
try {
proxyPort = Integer.parseInt(data[1]);
} catch (NumberFormatException e) {
return false;
}
}
final ProxyInfo p = ProxyInfo.buildDirectProxy(proxyHost, proxyPort,
Collections.emptyList());
setGlobalProxy(p);
return true;
}
return false;
}
/**
* Sends the system broadcast informing apps about a new proxy configuration.
*
* Confusingly this method also sets the PAC file URL. TODO : separate this, it has nothing
* to do in a "sendProxyBroadcast" method.
*/
public void sendProxyBroadcast() {
final ProxyInfo defaultProxy = getDefaultProxy();
final ProxyInfo proxyInfo = null != defaultProxy ?
defaultProxy : ProxyInfo.buildDirectProxy("", 0, Collections.emptyList());
mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo);
if (!shouldSendBroadcast(proxyInfo)) {
return;
}
if (DBG) Log.d(TAG, "sending Proxy Broadcast for " + proxyInfo);
Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING |
Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
intent.putExtra(EXTRA_PROXY_INFO, proxyInfo);
final long ident = Binder.clearCallingIdentity();
try {
mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
private boolean shouldSendBroadcast(ProxyInfo proxy) {
return Uri.EMPTY.equals(proxy.getPacFileUrl()) || proxy.getPort() > 0;
}
/**
* Sets the global proxy in memory. Also writes the values to the global settings of the device.
*
* @param proxyInfo the proxy spec, or null for no proxy.
*/
public void setGlobalProxy(@Nullable ProxyInfo proxyInfo) {
synchronized (mProxyLock) {
// ProxyInfo#equals is not commutative :( and is public API, so it can't be fixed.
if (proxyInfo == mGlobalProxy) return;
if (proxyInfo != null && proxyInfo.equals(mGlobalProxy)) return;
if (mGlobalProxy != null && mGlobalProxy.equals(proxyInfo)) return;
final String host;
final int port;
final String exclList;
final String pacFileUrl;
if (proxyInfo != null && (!TextUtils.isEmpty(proxyInfo.getHost()) ||
!Uri.EMPTY.equals(proxyInfo.getPacFileUrl()))) {
if (!proxyInfo.isValid()) {
if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo);
return;
}
mGlobalProxy = new ProxyInfo(proxyInfo);
host = mGlobalProxy.getHost();
port = mGlobalProxy.getPort();
exclList = ProxyUtils.exclusionListAsString(mGlobalProxy.getExclusionList());
pacFileUrl = Uri.EMPTY.equals(proxyInfo.getPacFileUrl())
? "" : proxyInfo.getPacFileUrl().toString();
} else {
host = "";
port = 0;
exclList = "";
pacFileUrl = "";
mGlobalProxy = null;
}
final ContentResolver res = mContext.getContentResolver();
final long token = Binder.clearCallingIdentity();
try {
Settings.Global.putString(res, GLOBAL_HTTP_PROXY_HOST, host);
Settings.Global.putInt(res, GLOBAL_HTTP_PROXY_PORT, port);
Settings.Global.putString(res, GLOBAL_HTTP_PROXY_EXCLUSION_LIST, exclList);
Settings.Global.putString(res, GLOBAL_HTTP_PROXY_PAC, pacFileUrl);
} finally {
Binder.restoreCallingIdentity(token);
}
sendProxyBroadcast();
}
}
/**
* Sets the default proxy for the device.
*
* The default proxy is the proxy used for networks that do not have a specific proxy.
* @param proxyInfo the proxy spec, or null for no proxy.
*/
public void setDefaultProxy(@Nullable ProxyInfo proxyInfo) {
synchronized (mProxyLock) {
if (Objects.equals(mDefaultProxy, proxyInfo)) return;
if (proxyInfo != null && !proxyInfo.isValid()) {
if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo);
return;
}
// This call could be coming from the PacProxyService, containing the port of the
// local proxy. If this new proxy matches the global proxy then copy this proxy to the
// global (to get the correct local port), and send a broadcast.
// TODO: Switch PacProxyService to have its own message to send back rather than
// reusing EVENT_HAS_CHANGED_PROXY and this call to handleApplyDefaultProxy.
if ((mGlobalProxy != null) && (proxyInfo != null)
&& (!Uri.EMPTY.equals(proxyInfo.getPacFileUrl()))
&& proxyInfo.getPacFileUrl().equals(mGlobalProxy.getPacFileUrl())) {
mGlobalProxy = proxyInfo;
sendProxyBroadcast();
return;
}
mDefaultProxy = proxyInfo;
if (mGlobalProxy != null) return;
if (mDefaultProxyEnabled) {
sendProxyBroadcast();
}
}
}
}
|