Add tests for PAC proxies.
Test: this is a change that adds a test
Bug: 138810051
Bug: 140610528
Change-Id: I6e7b77320f6baf32ac2f98634fa2e3231d1bf12f
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 51df1dd..4c4189c 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -861,8 +861,7 @@
// A helper object to track the current default HTTP proxy. ConnectivityService needs to tell
// the world when it changes.
- @VisibleForTesting
- protected final ProxyTracker mProxyTracker;
+ private final ProxyTracker mProxyTracker;
final private SettingsObserver mSettingsObserver;
@@ -1846,6 +1845,13 @@
mHandler.sendEmptyMessage(EVENT_INGRESS_RATE_LIMIT_CHANGED);
}
+ @VisibleForTesting
+ void simulateUpdateProxyInfo(@Nullable final Network network,
+ @NonNull final ProxyInfo proxyInfo) {
+ Message.obtain(mHandler, EVENT_PROXY_HAS_CHANGED,
+ new Pair<>(network, proxyInfo)).sendToTarget();
+ }
+
private void handleAlwaysOnNetworkRequest(
NetworkRequest networkRequest, String settingName, boolean defaultValue) {
final boolean enable = toBool(Settings.Global.getInt(
diff --git a/service/src/com/android/server/connectivity/ProxyTracker.java b/service/src/com/android/server/connectivity/ProxyTracker.java
index bc0929c..b3cbb2a 100644
--- a/service/src/com/android/server/connectivity/ProxyTracker.java
+++ b/service/src/com/android/server/connectivity/ProxyTracker.java
@@ -330,7 +330,7 @@
public void setDefaultProxy(@Nullable ProxyInfo proxyInfo) {
synchronized (mProxyLock) {
if (Objects.equals(mDefaultProxy, proxyInfo)) return;
- if (proxyInfo != null && !proxyInfo.isValid()) {
+ if (proxyInfo != null && !proxyInfo.isValid()) {
if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo);
return;
}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index e434649..0bbd406 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -11276,6 +11276,212 @@
assertEquals(testProxyInfo, mService.getProxyForNetwork(null));
}
+ /*
+ * Note for maintainers about how PAC proxies are implemented in Android.
+ *
+ * Generally, a proxy is just a hostname and a port to which requests are sent, instead of
+ * sending them directly. Most HTTP libraries know to use this protocol, and the Java
+ * language has properties to help handling these :
+ * https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html
+ * Unfortunately these properties are very old and do not take multi-networking into account.
+ *
+ * A PAC proxy consists of a javascript file stored on a server, and the client is expected to
+ * download the file and interpret the javascript for each HTTP request to know where to direct
+ * it. The device must therefore run code (namely, a javascript interpreter) to interpret the
+ * PAC file correctly. Most HTTP libraries do not know how to do this, since they do not
+ * embark a javascript interpreter (and it would be generally unreasonable for them to do
+ * so). Some apps, notably browsers, do know how to do this, but they are the exception rather
+ * than the rule.
+ * So to support most apps, Android embarks the javascript interpreter. When a network is
+ * configured to have a PAC proxy, Android will first set the ProxyInfo object to an object
+ * that contains the PAC URL (to communicate that to apps that know how to use it), then
+ * download the PAC file and start a native process which will open a server on localhost,
+ * and uses the interpreter inside WebView to interpret the PAC file. This server takes
+ * a little bit of time to start and will listen on a random port. When the port is known,
+ * the framework receives a notification and it updates the ProxyInfo in LinkProperties
+ * as well as send a broadcast to inform apps. This new ProxyInfo still contains the PAC URL,
+ * but it also contains "localhost" as the host and the port that the server listens to as
+ * the port. This will let HTTP libraries that don't have a javascript interpreter work,
+ * because they'll send the requests to this server running on localhost on the correct port,
+ * and this server will do what is appropriate with each request according to the PAC file.
+ *
+ * Note that at the time of this writing, Android does not support starting multiple local
+ * PAC servers, though this would be possible in theory by just starting multiple instances
+ * of this process running their server on different ports. As a stopgap measure, Android
+ * keeps one local server which is always the one for the default network. If a non-default
+ * network has a PAC proxy, it will have a LinkProperties with a port of -1, which means it
+ * could still be used by apps that know how to use the PAC URL directly, but not by HTTP
+ * libs that don't know how to do that. When a network with a PAC proxy becomes the default,
+ * the local server is started. When a network without a PAC proxy becomes the default, the
+ * local server is stopped if it is running (and the LP for the old default network should
+ * be reset to have a port of -1).
+ *
+ * In principle, each network can be configured with a different proxy (typically in the
+ * network settings for a Wi-Fi network). These end up exposed in the LinkProperties of the
+ * relevant network.
+ * Android also exposes ConnectivityManager#getDefaultProxy, which is simply the proxy for
+ * the default network. This was retrofitted from a time where Android did not support multiple
+ * concurrent networks, hence the difficult architecture.
+ * Note that there is also a "global" proxy that can be set by the DPM. When this is set,
+ * it overrides the proxy settings for every single network at the same time – that is, the
+ * system behaves as if the global proxy is the configured proxy for each network.
+ *
+ * Generally there are four ways for apps to look up the proxy.
+ * - Looking up the ProxyInfo in the LinkProperties for a network.
+ * - Listening to the PROXY_CHANGED_ACTION broadcast
+ * - Calling ConnectivityManager#getDefaultProxy, or ConnectivityManager#getProxyForNetwork
+ * which can be used to retrieve the proxy for any given network or the default network by
+ * passing null.
+ * - Reading the standard JVM properties (http{s,}.proxy{Host,Port}). See the Java
+ * distribution documentation for details on how these are supposed to work :
+ * https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html
+ * In Android, these are set by ActivityThread in each process in response to the broadcast.
+ * Many apps actually use these, and it's important they work because it's part of the
+ * Java standard, meaning they need to be set for existing Java libs to work on Android.
+ */
+ @Test
+ public void testPacProxy() throws Exception {
+ final Uri pacUrl = Uri.parse("https://pac-url");
+ mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+ mCellAgent.connect(true);
+
+ final TestNetworkCallback wifiCallback = new TestNetworkCallback();
+ final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI).build();
+ mCm.registerNetworkCallback(wifiRequest, wifiCallback);
+
+ mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+ mWiFiAgent.connect(true);
+ wifiCallback.expectAvailableThenValidatedCallbacks(mWiFiAgent);
+
+ final ProxyInfo initialProxyInfo = ProxyInfo.buildPacProxy(pacUrl);
+ final LinkProperties testLinkProperties = new LinkProperties();
+ testLinkProperties.setHttpProxy(initialProxyInfo);
+ mWiFiAgent.sendLinkProperties(testLinkProperties);
+ wifiCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mWiFiAgent);
+
+ // At first the local PAC proxy server is unstarted (see the description of what the local
+ // server is in the comment at the top of this method). It will contain the PAC URL and a
+ // port of -1 because it is unstarted. Check that all ways of getting that proxy info
+ // returns the same object that was initially created.
+ final ProxyInfo unstartedDefaultProxyInfo = mService.getProxyForNetwork(null);
+ final ProxyInfo unstartedWifiProxyInfo = mService.getProxyForNetwork(
+ mWiFiAgent.getNetwork());
+ final LinkProperties unstartedLp =
+ mService.getLinkProperties(mWiFiAgent.getNetwork());
+
+ assertEquals(initialProxyInfo, unstartedDefaultProxyInfo);
+ assertEquals(initialProxyInfo, unstartedWifiProxyInfo);
+ assertEquals(initialProxyInfo, unstartedLp.getHttpProxy());
+
+ // Make sure the cell network is unaffected. The LP are per-network and each network has
+ // its own configuration. The default proxy and broadcast are system-wide, and the JVM
+ // properties are per-process, and therefore can have only one value at any given time,
+ // so the code sets them to the proxy of the default network (TODO : really, since the
+ // default process is per-network, the JVM properties (http.proxyHost family – see
+ // the comment at the top of the method for details about these) also should be per-network
+ // and even the broadcast contents should be but none of this is implemented). The LP are
+ // still per-network, and a process that wants to use a non-default network is supposed to
+ // look up the proxy in its LP and it has to be correct.
+ assertNull(mService.getLinkProperties(mCellAgent.getNetwork()).getHttpProxy());
+ assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+
+ // Simulate PacManager sending the notification that the local server has started
+ final ProxyInfo servingProxyInfo = new ProxyInfo(pacUrl, 2097);
+ final ExpectedBroadcast b1 = expectProxyChangeAction(servingProxyInfo);
+ mService.simulateUpdateProxyInfo(mWiFiAgent.getNetwork(), servingProxyInfo);
+// wifiCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mWiFiAgent);
+ b1.expectBroadcast();
+
+ final ProxyInfo startedDefaultProxyInfo = mService.getProxyForNetwork(null);
+ final ProxyInfo startedWifiProxyInfo = mService.getProxyForNetwork(
+ mWiFiAgent.getNetwork());
+ final LinkProperties startedLp = mService.getLinkProperties(mWiFiAgent.getNetwork());
+ // TODO : activate these tests when b/138810051 is fixed.
+// assertEquals(servingProxyInfo, startedDefaultProxyInfo);
+// assertEquals(servingProxyInfo, startedWifiProxyInfo);
+// assertEquals(servingProxyInfo, startedLp.getHttpProxy());
+// // Make sure the cell network is still unaffected
+// assertNull(mService.getLinkProperties(mCellAgent.getNetwork()).getHttpProxy());
+// assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+//
+// final Uri ethPacUrl = Uri.parse("https://ethernet-pac-url");
+// final TestableNetworkCallback ethernetCallback = new TestableNetworkCallback();
+// final NetworkRequest ethernetRequest = new NetworkRequest.Builder()
+// .addTransportType(TRANSPORT_ETHERNET).build();
+// mEthernetAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET);
+// mCm.registerNetworkCallback(ethernetRequest, ethernetCallback);
+// mEthernetAgent.connect(true);
+// ethernetCallback.expectAvailableThenValidatedCallbacks(mEthernetAgent);
+// // Wifi is no longer the default, so it should get a callback to LP changed with a PAC
+// // proxy but a port of -1 (since the local proxy doesn't serve wifi now)
+// wifiCallback.expect(LINK_PROPERTIES_CHANGED, mWiFiAgent,
+// lp -> lp.getLp().getHttpProxy().getPort() == -1
+// && lp.getLp().getHttpProxy().isPacProxy());
+// wifiCallback.expect(CallbackEntry.LOSING, mWiFiAgent);
+//
+// final ProxyInfo ethProxy = ProxyInfo.buildPacProxy(ethPacUrl);
+// final LinkProperties ethLinkProperties = new LinkProperties();
+// ethLinkProperties.setHttpProxy(ethProxy);
+// mEthernetAgent.sendLinkProperties(ethLinkProperties);
+// ethernetCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mEthernetAgent);
+// // Default network is Ethernet
+// assertEquals(ethProxy, mService.getProxyForNetwork(null));
+// assertEquals(ethProxy, mService.getProxyForNetwork(mEthernetAgent.getNetwork()));
+// // Proxy info for WiFi ideally should be the old one with the old server still running,
+// // but as the PAC code only handles one server at any given time, this can't work. Wifi
+// // having null as a proxy also won't work (apps using WiFi will try to access the network
+// // without proxy) but is not as bad as having the new proxy (that would send requests
+// // over the default network).
+// assertEquals(initialProxyInfo, mService.getProxyForNetwork(mWiFiAgent.getNetwork()));
+// assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+//
+// final ProxyInfo servingEthProxy = new ProxyInfo(pacUrl, 2099);
+// final ExpectedBroadcast b2 = expectProxyChangeAction(servingEthProxy);
+// final ExpectedBroadcast b3 = expectProxyChangeAction(servingProxyInfo);
+//
+// mService.simulateUpdateProxyInfo(mEthernetAgent.getNetwork(), servingEthProxy);
+// ethernetCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mEthernetAgent);
+// assertEquals(servingEthProxy, mService.getProxyForNetwork(null));
+// assertEquals(servingEthProxy, mService.getProxyForNetwork(
+// mEthernetAgent.getNetwork()));
+// assertEquals(initialProxyInfo,
+// mService.getProxyForNetwork(mWiFiAgent.getNetwork()));
+// assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+// b2.expectBroadcast();
+//
+// // Ethernet disconnects, back to WiFi
+// mEthernetAgent.disconnect();
+// ethernetCallback.expect(CallbackEntry.LOST, mEthernetAgent);
+// wifiCallback.assertNoCallback();
+//
+// assertEquals(initialProxyInfo, mService.getProxyForNetwork(null));
+// assertEquals(initialProxyInfo,
+// mService.getProxyForNetwork(mWiFiAgent.getNetwork()));
+// assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+//
+// // After a while the PAC file for wifi is resolved again
+// mService.simulateUpdateProxyInfo(mWiFiAgent.getNetwork(), servingProxyInfo);
+// wifiCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mWiFiAgent);
+// assertEquals(servingProxyInfo, mService.getProxyForNetwork(null));
+// assertEquals(servingProxyInfo,
+// mService.getProxyForNetwork(mWiFiAgent.getNetwork()));
+// assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+// b3.expectBroadcast();
+//
+// // Expect a broadcast after wifi disconnected. The proxy might be default proxy or an
+// // empty proxy built by buildDirectProxy. See {@link ProxyTracker.sendProxyBroadcast}.
+// // Thus here expects a broadcast will be received but does not verify the content of the
+// // proxy.
+// final ExpectedBroadcast b4 = expectProxyChangeAction();
+// mWiFiAgent.disconnect();
+// b4.expectBroadcast();
+// wifiCallback.expect(CallbackEntry.LOST, mWiFiAgent);
+// assertNull(mService.getProxyForNetwork(null));
+ assertNull(mService.getLinkProperties(mCellAgent.getNetwork()).getHttpProxy());
+ assertNull(mService.getGlobalProxy());
+ }
+
@Test
public void testGetProxyForVPN() throws Exception {
final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888);