[Passpoint] Support for Terms & Conditions
Added framework support for Terms & Conditions. Handle the
WNM-notification and extract the URL. Reject invalid and non-HTTPS
URLs (block these networks since there will be no access without
accepting the T&Cs).
Bug: 171928337
Test: atest ClientModeImplTest PasspointManagerTest
Change-Id: Iaf161d258f7768337f919aeaf27174742c541a1f
diff --git a/service/java/com/android/server/wifi/ClientModeImpl.java b/service/java/com/android/server/wifi/ClientModeImpl.java
index 894239e..1b2159e 100644
--- a/service/java/com/android/server/wifi/ClientModeImpl.java
+++ b/service/java/com/android/server/wifi/ClientModeImpl.java
@@ -837,7 +837,7 @@
@Override
public void onProvisioningSuccess(LinkProperties newLp) {
- addPasspointVenueUrlToLinkProperties(newLp);
+ addPasspointUrlsToLinkProperties(newLp);
mWifiMetrics.logStaEvent(mInterfaceName, StaEvent.TYPE_CMD_IP_CONFIGURATION_SUCCESSFUL);
sendMessage(CMD_UPDATE_LINKPROPERTIES, newLp);
sendMessage(CMD_IP_CONFIGURATION_SUCCESSFUL);
@@ -851,7 +851,7 @@
@Override
public void onLinkPropertiesChange(LinkProperties newLp) {
- addPasspointVenueUrlToLinkProperties(newLp);
+ addPasspointUrlsToLinkProperties(newLp);
sendMessage(CMD_UPDATE_LINKPROPERTIES, newLp);
}
@@ -3500,11 +3500,16 @@
mPasspointManager.handleDeauthImminentEvent((WnmData) message.obj,
getConnectedWifiConfigurationInternal());
break;
+ case WifiMonitor.HS20_TERMS_AND_CONDITIONS_ACCEPTANCE_REQUIRED_EVENT:
+ if (!mPasspointManager.handleTermsAndConditionsEvent((WnmData) message.obj,
+ getConnectedWifiConfigurationInternal())) {
+ loge("Disconnecting from Passpoint network due to an issue with T&C");
+ sendMessage(CMD_DISCONNECT);
+ }
+ break;
case WifiMonitor.HS20_REMEDIATION_EVENT:
- case WifiMonitor.HS20_TERMS_AND_CONDITIONS_ACCEPTANCE_REQUIRED_EVENT: {
mPasspointManager.receivedWnmFrame((WnmData) message.obj);
break;
- }
case WifiMonitor.MBO_OCE_BSS_TM_HANDLING_DONE: {
handleBssTransitionRequest((BtmFrameData) message.obj);
break;
@@ -4066,6 +4071,7 @@
}
// When connecting to Passpoint, ask for the Venue URL
if (config.isPasspoint()) {
+ mPasspointManager.clearTermsAndConditionsUrl();
if (scanResult == null && mLastBssid != null) {
// The cached scan result of connected network would be null at the
// first connection, try to check full scan result list again to look up
@@ -4119,6 +4125,7 @@
if (!newConnectionInProgress) {
transitionTo(mDisconnectedState);
}
+ mPasspointManager.clearTermsAndConditionsUrl();
break;
}
case WifiMonitor.TARGET_BSSID_EVENT: {
@@ -5985,7 +5992,7 @@
return mWifiNative.getRxPktFates(mInterfaceName);
}
- private void addPasspointVenueUrlToLinkProperties(LinkProperties linkProperties) {
+ private void addPasspointUrlsToLinkProperties(LinkProperties linkProperties) {
WifiConfiguration currentNetwork = getConnectedWifiConfigurationInternal();
if (currentNetwork == null || !currentNetwork.isPasspoint()) {
return;
@@ -5996,14 +6003,29 @@
return;
}
URL venueUrl = mPasspointManager.getVenueUrl(scanResult);
- if (venueUrl != null) {
- // Update the Venue URL and the friendly name to populate the notification
- CaptivePortalData captivePortalData = new CaptivePortalData.Builder()
- .setVenueInfoUrl(Uri.parse(venueUrl.toString()))
- // TODO: Add when new API is available
- // .setVenueFriendlyName(currentNetwork.providerFriendlyName)
- .build();
- linkProperties.setCaptivePortalData(captivePortalData);
+ URL termsAndConditionsUrl = mPasspointManager.getTermsAndConditionsUrl();
+
+ if (venueUrl == null && termsAndConditionsUrl == null) {
+ return;
}
+
+ // Update the friendly name to populate the notification
+ CaptivePortalData.Builder captivePortalDataBuilder = new CaptivePortalData.Builder();
+ // TODO: Add when new API is available
+ // .setVenueFriendlyName(currentNetwork.providerFriendlyName);
+
+ // Update the Venue URL if available
+ if (venueUrl != null) {
+ captivePortalDataBuilder.setVenueInfoUrl(Uri.parse(venueUrl.toString()));
+ }
+
+ // Update the T&C URL if available. The network is captive if T&C URL is available
+ if (termsAndConditionsUrl != null) {
+ // TODO: Add when NetworkStack changes are implemented
+ //captivePortalDataBuilder.setUserPortalUrl(Uri.parse(termsAndConditionsUrl.toString()))
+ // .setCaptive(true);
+ }
+
+ linkProperties.setCaptivePortalData(captivePortalDataBuilder.build());
}
}
diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointManager.java b/service/java/com/android/server/wifi/hotspot2/PasspointManager.java
index a65ba32..d60390a 100644
--- a/service/java/com/android/server/wifi/hotspot2/PasspointManager.java
+++ b/service/java/com/android/server/wifi/hotspot2/PasspointManager.java
@@ -132,6 +132,7 @@
private final MacAddressUtil mMacAddressUtil;
private final Clock mClock;
private final WifiPermissionsUtil mWifiPermissionsUtil;
+ private URL mTermsAndConditionsUrl = null;
/**
* Map of package name of an app to the app ops changed listener for the app.
@@ -1498,9 +1499,21 @@
return;
}
- PasspointProvider provider = mProviders.get(config.getProfileKey());
+ blockProvider(config.getProfileKey(), event.getBssid(), event.isEss(), event.getDelay());
+ }
+
+ /**
+ * Block a specific provider from network selection
+ *
+ * @param passpointUniqueId The unique ID of the Passpoint network
+ * @param bssid BSSID of the AP
+ * @param isEss Block the ESS or the BSS
+ * @param delay Delay in seconds
+ */
+ private void blockProvider(String passpointUniqueId, long bssid, boolean isEss, int delay) {
+ PasspointProvider provider = mProviders.get(passpointUniqueId);
if (provider != null) {
- provider.blockBssOrEss(event.getBssid(), event.isEss(), event.getDelay());
+ provider.blockBssOrEss(bssid, isEss, delay);
}
}
@@ -1525,4 +1538,59 @@
mProviders.values().stream().forEach(p -> p.setAnonymousIdentity(null));
mWifiConfigManager.saveToStore(true);
}
+
+ /**
+ * Clears the Terms & Conditions URL, to be used upon a successful connection to Passpoint
+ */
+ public void clearTermsAndConditionsUrl() {
+ mTermsAndConditionsUrl = null;
+ }
+
+ /**
+ * Handle Terms & Conditions acceptance required WNM-Notification event
+ *
+ * @param event Terms & Conditions WNM-Notification data
+ * @param config Configuration of the currently connected Passpoint network
+ *
+ * @return true if Terms & conditions URL is valid, false otherwise
+ */
+ public boolean handleTermsAndConditionsEvent(WnmData event, WifiConfiguration config) {
+ if (event == null || config == null || !config.isPasspoint()) {
+ return false;
+ }
+ final int oneHourInSeconds = 60 * 60;
+ final int twentyFourHoursInSeconds = 24 * 60 * 60;
+ URL termsAndConditionsUrl;
+ try {
+ termsAndConditionsUrl = new URL(event.getUrl());
+ } catch (java.net.MalformedURLException e) {
+ Log.e(TAG, "Malformed T&C URL: " + event.getUrl() + " from BSSID: "
+ + Utils.macToString(event.getBssid()));
+
+ // Block this provider for an hour, this unlikely issue may be resolved shortly
+ blockProvider(config.getProfileKey(), event.getBssid(), true, oneHourInSeconds);
+ return false;
+ }
+ // Reject URLs that are not HTTPS
+ if (!TextUtils.equals(termsAndConditionsUrl.getProtocol(), "https")) {
+ Log.e(TAG, "Non-HTTPS T&C URL rejected: " + termsAndConditionsUrl
+ + " from BSSID: " + Utils.macToString(event.getBssid()));
+
+ // Block this provider for 24 hours, it is unlikely to be changed
+ blockProvider(config.getProfileKey(), event.getBssid(), true, twentyFourHoursInSeconds);
+ return false;
+ }
+ mTermsAndConditionsUrl = termsAndConditionsUrl;
+ return true;
+ }
+
+ /**
+ * Get the Terms & Conditions URL, if acceptance is required in this network
+ *
+ * @return URL to T&C website, null if not required by this network
+ */
+ @Nullable
+ public URL getTermsAndConditionsUrl() {
+ return mTermsAndConditionsUrl;
+ }
}
diff --git a/tests/wifitests/src/com/android/server/wifi/ClientModeImplTest.java b/tests/wifitests/src/com/android/server/wifi/ClientModeImplTest.java
index faff301..2d8bc98 100644
--- a/tests/wifitests/src/com/android/server/wifi/ClientModeImplTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/ClientModeImplTest.java
@@ -207,6 +207,8 @@
private static final int TEST_DELAY_IN_SECONDS = 300;
private static final int DEFINED_ERROR_CODE = 32764;
+ private static final String TEST_TERMS_AND_CONDITIONS_URL =
+ "https://policies.google.com/terms?hl=en-US";
private long mBinderToken;
private MockitoSession mSession;
@@ -1834,6 +1836,7 @@
verify(mWifiConnectivityManager).handleConnectionAttemptEnded(
any(), anyInt(), any(), any());
assertEquals("DisconnectedState", getCurrentState().getName());
+ verify(mPasspointManager).clearTermsAndConditionsUrl();
}
/**
@@ -5607,12 +5610,12 @@
}
/**
- * When connected to a Passpoint network, verify that the Venue URL is updated in the
- * {@link LinkProperties} object when provisioning complete and when link properties change
+ * When connected to a Passpoint network, verify that the Venue URL and T&C URL are updated in
+ * the {@link LinkProperties} object when provisioning complete and when link properties change
* events are received.
*/
@Test
- public void testVenueUrlUpdateForPasspointNetworks() throws Exception {
+ public void testVenueAndTCUrlsUpdateForPasspointNetworks() throws Exception {
setupPasspointConnection();
DhcpResultsParcelable dhcpResults = new DhcpResultsParcelable();
dhcpResults.baseConfiguration = new StaticIpConfiguration();
@@ -5626,6 +5629,46 @@
mLooper.dispatchAll();
mIpClientCallback.onLinkPropertiesChange(new LinkProperties());
mLooper.dispatchAll();
+ verify(mPasspointManager).clearTermsAndConditionsUrl();
verify(mPasspointManager, times(2)).getVenueUrl(any(ScanResult.class));
+ verify(mPasspointManager, times(2)).getTermsAndConditionsUrl();
+ }
+
+ /**
+ * Verify that the T&C WNM-Notification is handled by relaying to the Passpoint
+ * Manager.
+ */
+ @Test
+ public void testHandlePasspointTermsAndConditionsWnmNotification() throws Exception {
+ setupEapSimConnection();
+ WnmData wnmData = WnmData.createTermsAndConditionsAccetanceRequiredEvent(TEST_BSSID,
+ TEST_TERMS_AND_CONDITIONS_URL);
+ when(mPasspointManager.handleTermsAndConditionsEvent(eq(wnmData),
+ any(WifiConfiguration.class))).thenReturn(true);
+ mCmi.sendMessage(WifiMonitor.HS20_TERMS_AND_CONDITIONS_ACCEPTANCE_REQUIRED_EVENT,
+ 0, 0, wnmData);
+ mLooper.dispatchAll();
+ verify(mPasspointManager).handleTermsAndConditionsEvent(eq(wnmData),
+ any(WifiConfiguration.class));
+ verify(mWifiNative, never()).disconnect(anyString());
+ }
+
+ /**
+ * Verify that when a bad URL is received in the T&C WNM-Notification, the connection is
+ * disconnected.
+ */
+ @Test
+ public void testHandlePasspointTermsAndConditionsWnmNotificationWithBadUrl() throws Exception {
+ setupEapSimConnection();
+ WnmData wnmData = WnmData.createTermsAndConditionsAccetanceRequiredEvent(TEST_BSSID,
+ TEST_TERMS_AND_CONDITIONS_URL);
+ when(mPasspointManager.handleTermsAndConditionsEvent(eq(wnmData),
+ any(WifiConfiguration.class))).thenReturn(false);
+ mCmi.sendMessage(WifiMonitor.HS20_TERMS_AND_CONDITIONS_ACCEPTANCE_REQUIRED_EVENT,
+ 0, 0, wnmData);
+ mLooper.dispatchAll();
+ verify(mPasspointManager).handleTermsAndConditionsEvent(eq(wnmData),
+ any(WifiConfiguration.class));
+ verify(mWifiNative).disconnect(eq(WIFI_IFACE_NAME));
}
}
diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java
index cd96164..9d7c887 100644
--- a/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java
@@ -153,6 +153,12 @@
private static final String TEST_LOCALE_ENGLISH = "eng";
private static final String TEST_LOCALE_HEBREW = "heb";
private static final String TEST_LOCALE_SPANISH = "spa";
+ private static final String TEST_TERMS_AND_CONDITIONS_URL =
+ "https://policies.google.com/terms?hl=en-US";
+ private static final String TEST_TERMS_AND_CONDITIONS_URL_NON_HTTPS =
+ "http://policies.google.com/terms?hl=en-US";
+ private static final String TEST_TERMS_AND_CONDITIONS_URL_INVALID =
+ "httpps://policies.google.com/terms?hl=en-US";
private static final long TEST_BSSID = 0x112233445566L;
private static final String TEST_SSID = "TestSSID";
@@ -2938,5 +2944,65 @@
session.finishMocking();
}
}
+
+ /**
+ * Verify that Passpoint manager handles the terms and conditions URL correctly: Accepts only
+ * HTTPS URLs, and rejects HTTP and invalid URLs.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testHandleTermsAndConditionsEvent() throws Exception {
+ WifiConfiguration config = WifiConfigurationTestUtil.createPasspointNetwork();
+ PasspointProvider passpointProvider = addTestProvider(TEST_FQDN, TEST_FRIENDLY_NAME,
+ TEST_PACKAGE, config, false, null);
+ assertTrue(mManager.handleTermsAndConditionsEvent(
+ WnmData.createTermsAndConditionsAccetanceRequiredEvent(TEST_BSSID,
+ TEST_TERMS_AND_CONDITIONS_URL), config));
+
+ // Verify that this provider is never blocked
+ verify(passpointProvider, never()).blockBssOrEss(anyLong(), anyBoolean(), anyInt());
+
+ assertFalse(mManager.handleTermsAndConditionsEvent(
+ WnmData.createTermsAndConditionsAccetanceRequiredEvent(TEST_BSSID,
+ TEST_TERMS_AND_CONDITIONS_URL_NON_HTTPS), config));
+
+ // Verify that the ESS is blocked for 24 hours, the URL is non-HTTPS and unlikely to change
+ verify(passpointProvider).blockBssOrEss(eq(TEST_BSSID), eq(true), eq(24 * 60 * 60));
+
+ assertFalse(mManager.handleTermsAndConditionsEvent(
+ WnmData.createTermsAndConditionsAccetanceRequiredEvent(TEST_BSSID,
+ TEST_TERMS_AND_CONDITIONS_URL_INVALID), config));
+
+ // Verify that the ESS is blocked for an hour due to a temporary issue with the URL
+ verify(passpointProvider).blockBssOrEss(eq(TEST_BSSID), eq(true), eq(60 * 60));
+
+ // Now try with a non-Passpoint network
+ config = WifiConfigurationTestUtil.createEapNetwork();
+ assertFalse(mManager.handleTermsAndConditionsEvent(
+ WnmData.createTermsAndConditionsAccetanceRequiredEvent(TEST_BSSID,
+ TEST_TERMS_AND_CONDITIONS_URL), config));
+ // and a null configuration
+ assertFalse(mManager.handleTermsAndConditionsEvent(
+ WnmData.createTermsAndConditionsAccetanceRequiredEvent(TEST_BSSID,
+ TEST_TERMS_AND_CONDITIONS_URL), null));
+ }
+
+ /**
+ * Verify that Passpoint manager get and clear terms and conditions URL work as expected.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testGetAndClearTermsAndConditions() throws Exception {
+ WifiConfiguration config = WifiConfigurationTestUtil.createPasspointNetwork();
+ assertTrue(mManager.handleTermsAndConditionsEvent(
+ WnmData.createTermsAndConditionsAccetanceRequiredEvent(TEST_BSSID,
+ TEST_TERMS_AND_CONDITIONS_URL), config));
+ assertEquals(TEST_TERMS_AND_CONDITIONS_URL, mManager.getTermsAndConditionsUrl().toString());
+
+ mManager.clearTermsAndConditionsUrl();
+ assertTrue(mManager.getTermsAndConditionsUrl() == null);
+ }
}