From 0c28b97966c3b3379c7c5b59de98032971437378 Mon Sep 17 00:00:00 2001 From: Rajiv Ranjan Date: Tue, 19 Dec 2017 20:15:03 +0530 Subject: Revert "WifiConfigStore: Remove legacy modules" This reverts commit a83bd15861ae7175b1f62bc2dc8de63e0dbe808e and eventually support loading saved network creds from wpa_supplicant.conf to WifiConfigStore.xml file. CRs-Fixed: 2161641 Change-Id: Ic8222be386bc5064656b9df3f2f1a6af28de73b0 --- .../com/android/server/wifi/WifiConfigManager.java | 50 +- .../android/server/wifi/WifiConfigStoreLegacy.java | 354 ++++++++++++ .../java/com/android/server/wifi/WifiInjector.java | 9 +- .../android/server/wifi/WifiNetworkHistory.java | 630 +++++++++++++++++++++ .../com/android/server/wifi/WifiStateMachine.java | 4 + .../wifi/hotspot2/LegacyPasspointConfigParser.java | 513 +++++++++++++++++ 6 files changed, 1557 insertions(+), 3 deletions(-) create mode 100644 service/java/com/android/server/wifi/WifiConfigStoreLegacy.java create mode 100644 service/java/com/android/server/wifi/WifiNetworkHistory.java create mode 100644 service/java/com/android/server/wifi/hotspot2/LegacyPasspointConfigParser.java (limited to 'service/java/com') diff --git a/service/java/com/android/server/wifi/WifiConfigManager.java b/service/java/com/android/server/wifi/WifiConfigManager.java index 02f8302f9..11ae10b0d 100644 --- a/service/java/com/android/server/wifi/WifiConfigManager.java +++ b/service/java/com/android/server/wifi/WifiConfigManager.java @@ -47,6 +47,7 @@ import android.util.Log; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.server.LocalServices; +import com.android.server.wifi.WifiConfigStoreLegacy.WifiConfigStoreDataLegacy; import com.android.server.wifi.hotspot2.PasspointManager; import com.android.server.wifi.util.TelephonyUtil; import com.android.server.wifi.util.WifiPermissionsUtil; @@ -247,6 +248,7 @@ public class WifiConfigManager { private final TelephonyManager mTelephonyManager; private final WifiKeyStore mWifiKeyStore; private final WifiConfigStore mWifiConfigStore; + private final WifiConfigStoreLegacy mWifiConfigStoreLegacy; private final WifiPermissionsUtil mWifiPermissionsUtil; private final WifiPermissionsWrapper mWifiPermissionsWrapper; /** @@ -340,7 +342,7 @@ public class WifiConfigManager { WifiConfigManager( Context context, Clock clock, UserManager userManager, TelephonyManager telephonyManager, WifiKeyStore wifiKeyStore, - WifiConfigStore wifiConfigStore, + WifiConfigStore wifiConfigStore, WifiConfigStoreLegacy wifiConfigStoreLegacy, WifiPermissionsUtil wifiPermissionsUtil, WifiPermissionsWrapper wifiPermissionsWrapper, NetworkListStoreData networkListStoreData, @@ -352,6 +354,7 @@ public class WifiConfigManager { mTelephonyManager = telephonyManager; mWifiKeyStore = wifiKeyStore; mWifiConfigStore = wifiConfigStore; + mWifiConfigStoreLegacy = wifiConfigStoreLegacy; mWifiPermissionsUtil = wifiPermissionsUtil; mWifiPermissionsWrapper = wifiPermissionsWrapper; @@ -2701,6 +2704,46 @@ public class WifiConfigManager { mPendingStoreRead = false; } + /** + * Migrate data from legacy store files. The function performs the following operations: + * 1. Check if the legacy store files are present and the new store files are absent on device. + * 2. Read all the data from the store files. + * 3. Save it to the new store files. + * 4. Delete the legacy store file. + * + * @return true if migration was successful or not needed (fresh install), false if it failed. + */ + public boolean migrateFromLegacyStore() { + if (!mWifiConfigStoreLegacy.areStoresPresent()) { + Log.d(TAG, "Legacy store files not found. No migration needed!"); + return true; + } + if (mWifiConfigStore.areStoresPresent()) { + Log.d(TAG, "New store files found. No migration needed!" + + " Remove legacy store files"); + mWifiConfigStoreLegacy.removeStores(); + return true; + } + WifiConfigStoreDataLegacy storeData = mWifiConfigStoreLegacy.read(); + Log.d(TAG, "Reading from legacy store completed"); + loadInternalData(storeData.getConfigurations(), new ArrayList(), + storeData.getDeletedEphemeralSSIDs()); + + // Setup user store for the current user in case it have not setup yet, so that data + // owned by the current user will be backed to the user store. + if (mDeferredUserUnlockRead) { + mWifiConfigStore.setUserStore(WifiConfigStore.createUserFile(mCurrentUserId)); + mDeferredUserUnlockRead = false; + } + + if (!saveToStore(true)) { + return false; + } + mWifiConfigStoreLegacy.removeStores(); + Log.d(TAG, "Migration from legacy store completed"); + return true; + } + /** * Read the config store and load the in-memory lists from the store data retrieved and sends * out the networks changed broadcast. @@ -2715,7 +2758,10 @@ public class WifiConfigManager { public boolean loadFromStore() { if (!mWifiConfigStore.areStoresPresent()) { Log.d(TAG, "New store files not found. No saved networks loaded!"); - mPendingStoreRead = false; + if (!mWifiConfigStoreLegacy.areStoresPresent()) { + // No legacy store files either, so reset the pending store read flag. + mPendingStoreRead = false; + } return true; } // If the user unlock comes in before we load from store, which means the user store have diff --git a/service/java/com/android/server/wifi/WifiConfigStoreLegacy.java b/service/java/com/android/server/wifi/WifiConfigStoreLegacy.java new file mode 100644 index 000000000..39e48a5cb --- /dev/null +++ b/service/java/com/android/server/wifi/WifiConfigStoreLegacy.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2016 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.wifi; + +import android.net.IpConfiguration; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiEnterpriseConfig; +import android.os.Environment; +import android.util.Log; +import android.util.SparseArray; + +import com.android.server.net.IpConfigStore; +import com.android.server.wifi.hotspot2.LegacyPasspointConfig; +import com.android.server.wifi.hotspot2.LegacyPasspointConfigParser; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * This class provides the API's to load network configurations from legacy store + * mechanism (Pre O release). + * This class loads network configurations from: + * 1. /data/misc/wifi/networkHistory.txt + * 2. /data/misc/wifi/wpa_supplicant.conf + * 3. /data/misc/wifi/ipconfig.txt + * 4. /data/misc/wifi/PerProviderSubscription.conf + * + * The order of invocation of the public methods during migration is the following: + * 1. Check if legacy stores are present using {@link #areStoresPresent()}. + * 2. Load all the store data using {@link #read()} + * 3. Write the store data to the new store. + * 4. Remove all the legacy stores using {@link #removeStores()} + * + * NOTE: This class should only be used from WifiConfigManager and is not thread-safe! + * + * TODO(b/31065385): Passpoint config store data migration & deletion. + */ +public class WifiConfigStoreLegacy { + /** + * Log tag. + */ + private static final String TAG = "WifiConfigStoreLegacy"; + /** + * NetworkHistory config store file path. + */ + private static final File NETWORK_HISTORY_FILE = + new File(WifiNetworkHistory.NETWORK_HISTORY_CONFIG_FILE); + /** + * Passpoint config store file path. + */ + private static final File PPS_FILE = + new File(Environment.getDataMiscDirectory(), "wifi/PerProviderSubscription.conf"); + /** + * IpConfig config store file path. + */ + private static final File IP_CONFIG_FILE = + new File(Environment.getDataMiscDirectory(), "wifi/ipconfig.txt"); + /** + * List of external dependencies for WifiConfigManager. + */ + private final WifiNetworkHistory mWifiNetworkHistory; + private final WifiNative mWifiNative; + private final IpConfigStore mIpconfigStore; + + private final LegacyPasspointConfigParser mPasspointConfigParser; + + WifiConfigStoreLegacy(WifiNetworkHistory wifiNetworkHistory, + WifiNative wifiNative, IpConfigStore ipConfigStore, + LegacyPasspointConfigParser passpointConfigParser) { + mWifiNetworkHistory = wifiNetworkHistory; + mWifiNative = wifiNative; + mIpconfigStore = ipConfigStore; + mPasspointConfigParser = passpointConfigParser; + } + + /** + * Helper function to lookup the WifiConfiguration object from configKey to WifiConfiguration + * object map using the hashcode of the configKey. + * + * @param configurationMap Map of configKey to WifiConfiguration object. + * @param hashCode hash code of the configKey to match. + * @return + */ + private static WifiConfiguration lookupWifiConfigurationUsingConfigKeyHash( + Map configurationMap, int hashCode) { + for (Map.Entry entry : configurationMap.entrySet()) { + if (entry.getKey().hashCode() == hashCode) { + return entry.getValue(); + } + } + return null; + } + + /** + * Helper function to load {@link IpConfiguration} data from the ip config store file and + * populate the provided configuration map. + * + * @param configurationMap Map of configKey to WifiConfiguration object. + */ + private void loadFromIpConfigStore(Map configurationMap) { + // This is a map of the hash code of the network's configKey to the corresponding + // IpConfiguration. + SparseArray ipConfigurations = + mIpconfigStore.readIpAndProxyConfigurations(IP_CONFIG_FILE.getAbsolutePath()); + if (ipConfigurations == null || ipConfigurations.size() == 0) { + Log.w(TAG, "No ip configurations found in ipconfig store"); + return; + } + for (int i = 0; i < ipConfigurations.size(); i++) { + int id = ipConfigurations.keyAt(i); + WifiConfiguration config = + lookupWifiConfigurationUsingConfigKeyHash(configurationMap, id); + // This is the only place the map is looked up through a (dangerous) hash-value! + if (config == null || config.ephemeral) { + Log.w(TAG, "configuration found for missing network, nid=" + id + + ", ignored, networks.size=" + Integer.toString(ipConfigurations.size())); + } else { + config.setIpConfiguration(ipConfigurations.valueAt(i)); + } + } + } + + /** + * Helper function to load {@link WifiConfiguration} data from networkHistory file and populate + * the provided configuration map and deleted ephemeral ssid list. + * + * @param configurationMap Map of configKey to WifiConfiguration object. + * @param deletedEphemeralSSIDs Map of configKey to WifiConfiguration object. + */ + private void loadFromNetworkHistory( + Map configurationMap, Set deletedEphemeralSSIDs) { + // TODO: Need to revisit the scan detail cache persistance. We're not doing it in the new + // config store, so ignore it here as well. + Map scanDetailCaches = new HashMap<>(); + mWifiNetworkHistory.readNetworkHistory( + configurationMap, scanDetailCaches, deletedEphemeralSSIDs); + } + + /** + * Helper function to load {@link WifiConfiguration} data from wpa_supplicant and populate + * the provided configuration map and network extras. + * + * This method needs to manually parse the wpa_supplicant.conf file to retrieve some of the + * password fields like psk, wep_keys. password, etc. + * + * @param configurationMap Map of configKey to WifiConfiguration object. + * @param networkExtras Map of network extras parsed from wpa_supplicant. + */ + private void loadFromWpaSupplicant( + Map configurationMap, + SparseArray> networkExtras) { + if (!mWifiNative.migrateNetworksFromSupplicant(configurationMap, networkExtras)) { + Log.wtf(TAG, "Failed to load wifi configurations from wpa_supplicant"); + return; + } + if (configurationMap.isEmpty()) { + Log.w(TAG, "No wifi configurations found in wpa_supplicant"); + return; + } + } + + /** + * Helper function to update {@link WifiConfiguration} that represents a Passpoint + * configuration. + * + * This method will manually parse PerProviderSubscription.conf file to retrieve missing + * fields: provider friendly name, roaming consortium OIs, realm, IMSI. + * + * @param configurationMap Map of configKey to WifiConfiguration object. + * @param networkExtras Map of network extras parsed from wpa_supplicant. + */ + private void loadFromPasspointConfigStore( + Map configurationMap, + SparseArray> networkExtras) { + Map passpointConfigMap = null; + try { + passpointConfigMap = mPasspointConfigParser.parseConfig(PPS_FILE.getAbsolutePath()); + } catch (IOException e) { + Log.w(TAG, "Failed to read/parse Passpoint config file: " + e.getMessage()); + } + + List entriesToBeRemoved = new ArrayList<>(); + for (Map.Entry entry : configurationMap.entrySet()) { + WifiConfiguration wifiConfig = entry.getValue(); + // Ignore non-Enterprise network since enterprise configuration is required for + // Passpoint. + if (wifiConfig.enterpriseConfig == null || wifiConfig.enterpriseConfig.getEapMethod() + == WifiEnterpriseConfig.Eap.NONE) { + continue; + } + // Ignore configuration without FQDN. + Map extras = networkExtras.get(wifiConfig.networkId); + if (extras == null || !extras.containsKey(SupplicantStaNetworkHal.ID_STRING_KEY_FQDN)) { + continue; + } + String fqdn = networkExtras.get(wifiConfig.networkId).get( + SupplicantStaNetworkHal.ID_STRING_KEY_FQDN); + + // Remove the configuration if failed to find the matching configuration in the + // Passpoint configuration file. + if (passpointConfigMap == null || !passpointConfigMap.containsKey(fqdn)) { + entriesToBeRemoved.add(entry.getKey()); + continue; + } + + // Update the missing Passpoint configuration fields to this WifiConfiguration. + LegacyPasspointConfig passpointConfig = passpointConfigMap.get(fqdn); + wifiConfig.isLegacyPasspointConfig = true; + wifiConfig.FQDN = fqdn; + wifiConfig.providerFriendlyName = passpointConfig.mFriendlyName; + if (passpointConfig.mRoamingConsortiumOis != null) { + wifiConfig.roamingConsortiumIds = Arrays.copyOf( + passpointConfig.mRoamingConsortiumOis, + passpointConfig.mRoamingConsortiumOis.length); + } + if (passpointConfig.mImsi != null) { + wifiConfig.enterpriseConfig.setPlmn(passpointConfig.mImsi); + } + if (passpointConfig.mRealm != null) { + wifiConfig.enterpriseConfig.setRealm(passpointConfig.mRealm); + } + } + + // Remove any incomplete Passpoint configurations. Should never happen, in case it does + // remove them to avoid maintaining any invalid Passpoint configurations. + for (String key : entriesToBeRemoved) { + Log.w(TAG, "Remove incomplete Passpoint configuration: " + key); + configurationMap.remove(key); + } + } + + /** + * Helper function to load from the different legacy stores: + * 1. Read the network configurations from wpa_supplicant using {@link WifiNative}. + * 2. Read the network configurations from networkHistory.txt using {@link WifiNetworkHistory}. + * 3. Read the Ip configurations from ipconfig.txt using {@link IpConfigStore}. + * 4. Read all the passpoint info from PerProviderSubscription.conf using + * {@link LegacyPasspointConfigParser}. + */ + public WifiConfigStoreDataLegacy read() { + final Map configurationMap = new HashMap<>(); + final SparseArray> networkExtras = new SparseArray<>(); + final Set deletedEphemeralSSIDs = new HashSet<>(); + + loadFromWpaSupplicant(configurationMap, networkExtras); + loadFromNetworkHistory(configurationMap, deletedEphemeralSSIDs); + loadFromIpConfigStore(configurationMap); + loadFromPasspointConfigStore(configurationMap, networkExtras); + + // Now create config store data instance to be returned. + return new WifiConfigStoreDataLegacy( + new ArrayList<>(configurationMap.values()), deletedEphemeralSSIDs); + } + + /** + * Function to check if the legacy store files are present and hence load from those stores and + * then delete them. + * + * @return true if legacy store files are present, false otherwise. + */ + public boolean areStoresPresent() { + // We may have to keep the wpa_supplicant.conf file around. So, just use networkhistory.txt + // as a check to see if we have not yet migrated or not. This should be the last file + // that is deleted after migration. + File file = new File(WifiNetworkHistory.NETWORK_HISTORY_CONFIG_FILE); + return file.exists(); + } + + /** + * Method to remove all the legacy store files. This should only be invoked once all + * the data has been migrated to the new store file. + * 1. Removes all networks from wpa_supplicant and saves it to wpa_supplicant.conf + * 2. Deletes ipconfig.txt + * 3. Deletes networkHistory.txt + * + * @return true if all the store files were deleted successfully, false otherwise. + */ + public boolean removeStores() { + // TODO(b/29352330): Delete wpa_supplicant.conf file instead. + // First remove all networks from wpa_supplicant and save configuration. + if (!mWifiNative.removeAllNetworks()) { + Log.e(TAG, "Removing networks from wpa_supplicant failed"); + } + + // Now remove the ipconfig.txt file. + if (!IP_CONFIG_FILE.delete()) { + Log.e(TAG, "Removing ipconfig.txt failed"); + } + + // Now finally remove network history.txt + if (!NETWORK_HISTORY_FILE.delete()) { + Log.e(TAG, "Removing networkHistory.txt failed"); + } + + if (!PPS_FILE.delete()) { + Log.e(TAG, "Removing PerProviderSubscription.conf failed"); + } + + Log.i(TAG, "All legacy stores removed!"); + return true; + } + + /** + * Interface used to set a masked value in the provided configuration. The masked value is + * retrieved by parsing the wpa_supplicant.conf file. + */ + private interface MaskedWpaSupplicantFieldSetter { + void setValue(WifiConfiguration config, String value); + } + + /** + * Class used to encapsulate all the store data retrieved from the legacy (Pre O) store files. + */ + public static class WifiConfigStoreDataLegacy { + private List mConfigurations; + private Set mDeletedEphemeralSSIDs; + // private List mHomeSps; + + WifiConfigStoreDataLegacy(List configurations, + Set deletedEphemeralSSIDs) { + mConfigurations = configurations; + mDeletedEphemeralSSIDs = deletedEphemeralSSIDs; + } + + public List getConfigurations() { + return mConfigurations; + } + + public Set getDeletedEphemeralSSIDs() { + return mDeletedEphemeralSSIDs; + } + } +} diff --git a/service/java/com/android/server/wifi/WifiInjector.java b/service/java/com/android/server/wifi/WifiInjector.java index 4b8e6829e..b752e347f 100644 --- a/service/java/com/android/server/wifi/WifiInjector.java +++ b/service/java/com/android/server/wifi/WifiInjector.java @@ -45,6 +45,7 @@ import com.android.server.am.BatteryStatsService; import com.android.server.net.DelayedDiskWrite; import com.android.server.net.IpConfigStore; import com.android.server.wifi.aware.WifiAwareMetrics; +import com.android.server.wifi.hotspot2.LegacyPasspointConfigParser; import com.android.server.wifi.hotspot2.PasspointManager; import com.android.server.wifi.hotspot2.PasspointNetworkEvaluator; import com.android.server.wifi.hotspot2.PasspointObjectFactory; @@ -100,7 +101,9 @@ public class WifiInjector { private final WifiMulticastLockManager mWifiMulticastLockManager; private final WifiConfigStore mWifiConfigStore; private final WifiKeyStore mWifiKeyStore; + private final WifiNetworkHistory mWifiNetworkHistory; private final IpConfigStore mIpConfigStore; + private final WifiConfigStoreLegacy mWifiConfigStoreLegacy; private final WifiConfigManager mWifiConfigManager; private final WifiConnectivityHelper mWifiConnectivityHelper; private final LocalLog mConnectivityLocalLog; @@ -192,11 +195,15 @@ public class WifiInjector { WifiConfigStore.createSharedFile()); // Legacy config store DelayedDiskWrite writer = new DelayedDiskWrite(); + mWifiNetworkHistory = new WifiNetworkHistory(mContext, writer); mIpConfigStore = new IpConfigStore(writer); + mWifiConfigStoreLegacy = new WifiConfigStoreLegacy( + mWifiNetworkHistory, mWifiNative, mIpConfigStore, + new LegacyPasspointConfigParser()); // Config Manager mWifiConfigManager = new WifiConfigManager(mContext, mClock, UserManager.get(mContext), TelephonyManager.from(mContext), - mWifiKeyStore, mWifiConfigStore, mWifiPermissionsUtil, + mWifiKeyStore, mWifiConfigStore, mWifiConfigStoreLegacy, mWifiPermissionsUtil, mWifiPermissionsWrapper, new NetworkListStoreData(mContext), new DeletedEphemeralSsidsStoreData()); mWifiMetrics.setWifiConfigManager(mWifiConfigManager); diff --git a/service/java/com/android/server/wifi/WifiNetworkHistory.java b/service/java/com/android/server/wifi/WifiNetworkHistory.java new file mode 100644 index 000000000..282f6057d --- /dev/null +++ b/service/java/com/android/server/wifi/WifiNetworkHistory.java @@ -0,0 +1,630 @@ +/* + * Copyright (C) 2016 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.wifi; + +import android.content.Context; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiConfiguration.NetworkSelectionStatus; +import android.net.wifi.WifiSsid; +import android.os.Environment; +import android.os.Process; +import android.text.TextUtils; +import android.util.Log; + +import com.android.server.net.DelayedDiskWrite; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.text.DateFormat; +import java.util.BitSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Provides an API to read and write the network history from WifiConfigurations to file + * This is largely separate and extra to the supplicant config file. + */ +public class WifiNetworkHistory { + public static final String TAG = "WifiNetworkHistory"; + private static final boolean DBG = true; + private static final boolean VDBG = true; + static final String NETWORK_HISTORY_CONFIG_FILE = Environment.getDataDirectory() + + "/misc/wifi/networkHistory.txt"; + /* Network History Keys */ + private static final String SSID_KEY = "SSID"; + static final String CONFIG_KEY = "CONFIG"; + private static final String CONFIG_BSSID_KEY = "CONFIG_BSSID"; + private static final String CHOICE_KEY = "CHOICE"; + private static final String CHOICE_TIME_KEY = "CHOICE_TIME"; + private static final String LINK_KEY = "LINK"; + private static final String BSSID_KEY = "BSSID"; + private static final String BSSID_KEY_END = "/BSSID"; + private static final String RSSI_KEY = "RSSI"; + private static final String FREQ_KEY = "FREQ"; + private static final String DATE_KEY = "DATE"; + private static final String MILLI_KEY = "MILLI"; + private static final String NETWORK_ID_KEY = "ID"; + private static final String PRIORITY_KEY = "PRIORITY"; + private static final String DEFAULT_GW_KEY = "DEFAULT_GW"; + private static final String AUTH_KEY = "AUTH"; + private static final String BSSID_STATUS_KEY = "BSSID_STATUS"; + private static final String SELF_ADDED_KEY = "SELF_ADDED"; + private static final String DID_SELF_ADD_KEY = "DID_SELF_ADD"; + private static final String PEER_CONFIGURATION_KEY = "PEER_CONFIGURATION"; + static final String CREATOR_UID_KEY = "CREATOR_UID_KEY"; + private static final String CONNECT_UID_KEY = "CONNECT_UID_KEY"; + private static final String UPDATE_UID_KEY = "UPDATE_UID"; + private static final String FQDN_KEY = "FQDN"; + private static final String SCORER_OVERRIDE_KEY = "SCORER_OVERRIDE"; + private static final String SCORER_OVERRIDE_AND_SWITCH_KEY = "SCORER_OVERRIDE_AND_SWITCH"; + private static final String VALIDATED_INTERNET_ACCESS_KEY = "VALIDATED_INTERNET_ACCESS"; + private static final String NO_INTERNET_ACCESS_REPORTS_KEY = "NO_INTERNET_ACCESS_REPORTS"; + private static final String NO_INTERNET_ACCESS_EXPECTED_KEY = "NO_INTERNET_ACCESS_EXPECTED"; + private static final String EPHEMERAL_KEY = "EPHEMERAL"; + private static final String USE_EXTERNAL_SCORES_KEY = "USE_EXTERNAL_SCORES"; + private static final String METERED_HINT_KEY = "METERED_HINT"; + private static final String METERED_OVERRIDE_KEY = "METERED_OVERRIDE"; + private static final String NUM_ASSOCIATION_KEY = "NUM_ASSOCIATION"; + private static final String DELETED_EPHEMERAL_KEY = "DELETED_EPHEMERAL"; + private static final String CREATOR_NAME_KEY = "CREATOR_NAME"; + private static final String UPDATE_NAME_KEY = "UPDATE_NAME"; + private static final String USER_APPROVED_KEY = "USER_APPROVED"; + private static final String CREATION_TIME_KEY = "CREATION_TIME"; + private static final String UPDATE_TIME_KEY = "UPDATE_TIME"; + static final String SHARED_KEY = "SHARED"; + private static final String NETWORK_SELECTION_STATUS_KEY = "NETWORK_SELECTION_STATUS"; + private static final String NETWORK_SELECTION_DISABLE_REASON_KEY = + "NETWORK_SELECTION_DISABLE_REASON"; + private static final String HAS_EVER_CONNECTED_KEY = "HAS_EVER_CONNECTED"; + + private static final String SEPARATOR = ": "; + private static final String NL = "\n"; + + protected final DelayedDiskWrite mWriter; + Context mContext; + /* + * Lost config list, whenever we read a config from networkHistory.txt that was not in + * wpa_supplicant.conf + */ + HashSet mLostConfigsDbg = new HashSet(); + + public WifiNetworkHistory(Context c, DelayedDiskWrite writer) { + mContext = c; + mWriter = writer; + } + + /** + * Write network history to file, for configured networks + * + * @param networks List of ConfiguredNetworks to write to NetworkHistory + */ + public void writeKnownNetworkHistory(final List networks, + final ConcurrentHashMap scanDetailCaches, + final Set deletedEphemeralSSIDs) { + + /* Make a copy */ + //final List networks = new ArrayList(); + + //for (WifiConfiguration config : mConfiguredNetworks.valuesForAllUsers()) { + // networks.add(new WifiConfiguration(config)); + //} + + mWriter.write(NETWORK_HISTORY_CONFIG_FILE, new DelayedDiskWrite.Writer() { + public void onWriteCalled(DataOutputStream out) throws IOException { + for (WifiConfiguration config : networks) { + //loge("onWriteCalled write SSID: " + config.SSID); + /* if (config.getLinkProperties() != null) + loge(" lp " + config.getLinkProperties().toString()); + else + loge("attempt config w/o lp"); + */ + NetworkSelectionStatus status = config.getNetworkSelectionStatus(); + if (VDBG) { + int numlink = 0; + if (config.linkedConfigurations != null) { + numlink = config.linkedConfigurations.size(); + } + String disableTime; + if (config.getNetworkSelectionStatus().isNetworkEnabled()) { + disableTime = ""; + } else { + disableTime = "Disable time: " + DateFormat.getInstance().format( + config.getNetworkSelectionStatus().getDisableTime()); + } + logd("saving network history: " + config.configKey() + " gw: " + + config.defaultGwMacAddress + " Network Selection-status: " + + status.getNetworkStatusString() + + disableTime + " ephemeral=" + config.ephemeral + + " choice:" + status.getConnectChoice() + + " link:" + numlink + + " status:" + config.status + + " nid:" + config.networkId + + " hasEverConnected: " + status.getHasEverConnected()); + } + + if (!isValid(config)) { + continue; + } + + if (config.SSID == null) { + if (VDBG) { + logv("writeKnownNetworkHistory trying to write config with null SSID"); + } + continue; + } + if (VDBG) { + logv("writeKnownNetworkHistory write config " + config.configKey()); + } + out.writeUTF(CONFIG_KEY + SEPARATOR + config.configKey() + NL); + + if (config.SSID != null) { + out.writeUTF(SSID_KEY + SEPARATOR + config.SSID + NL); + } + if (config.BSSID != null) { + out.writeUTF(CONFIG_BSSID_KEY + SEPARATOR + config.BSSID + NL); + } else { + out.writeUTF(CONFIG_BSSID_KEY + SEPARATOR + "null" + NL); + } + if (config.FQDN != null) { + out.writeUTF(FQDN_KEY + SEPARATOR + config.FQDN + NL); + } + + out.writeUTF(PRIORITY_KEY + SEPARATOR + Integer.toString(config.priority) + NL); + out.writeUTF(NETWORK_ID_KEY + SEPARATOR + + Integer.toString(config.networkId) + NL); + out.writeUTF(SELF_ADDED_KEY + SEPARATOR + + Boolean.toString(config.selfAdded) + NL); + out.writeUTF(DID_SELF_ADD_KEY + SEPARATOR + + Boolean.toString(config.didSelfAdd) + NL); + out.writeUTF(NO_INTERNET_ACCESS_REPORTS_KEY + SEPARATOR + + Integer.toString(config.numNoInternetAccessReports) + NL); + out.writeUTF(VALIDATED_INTERNET_ACCESS_KEY + SEPARATOR + + Boolean.toString(config.validatedInternetAccess) + NL); + out.writeUTF(NO_INTERNET_ACCESS_EXPECTED_KEY + SEPARATOR + + Boolean.toString(config.noInternetAccessExpected) + NL); + out.writeUTF(EPHEMERAL_KEY + SEPARATOR + + Boolean.toString(config.ephemeral) + NL); + out.writeUTF(METERED_HINT_KEY + SEPARATOR + + Boolean.toString(config.meteredHint) + NL); + out.writeUTF(METERED_OVERRIDE_KEY + SEPARATOR + + Integer.toString(config.meteredOverride) + NL); + out.writeUTF(USE_EXTERNAL_SCORES_KEY + SEPARATOR + + Boolean.toString(config.useExternalScores) + NL); + if (config.creationTime != null) { + out.writeUTF(CREATION_TIME_KEY + SEPARATOR + config.creationTime + NL); + } + if (config.updateTime != null) { + out.writeUTF(UPDATE_TIME_KEY + SEPARATOR + config.updateTime + NL); + } + if (config.peerWifiConfiguration != null) { + out.writeUTF(PEER_CONFIGURATION_KEY + SEPARATOR + + config.peerWifiConfiguration + NL); + } + out.writeUTF(SCORER_OVERRIDE_KEY + SEPARATOR + + Integer.toString(config.numScorerOverride) + NL); + out.writeUTF(SCORER_OVERRIDE_AND_SWITCH_KEY + SEPARATOR + + Integer.toString(config.numScorerOverrideAndSwitchedNetwork) + NL); + out.writeUTF(NUM_ASSOCIATION_KEY + SEPARATOR + + Integer.toString(config.numAssociation) + NL); + out.writeUTF(CREATOR_UID_KEY + SEPARATOR + + Integer.toString(config.creatorUid) + NL); + out.writeUTF(CONNECT_UID_KEY + SEPARATOR + + Integer.toString(config.lastConnectUid) + NL); + out.writeUTF(UPDATE_UID_KEY + SEPARATOR + + Integer.toString(config.lastUpdateUid) + NL); + out.writeUTF(CREATOR_NAME_KEY + SEPARATOR + + config.creatorName + NL); + out.writeUTF(UPDATE_NAME_KEY + SEPARATOR + + config.lastUpdateName + NL); + out.writeUTF(USER_APPROVED_KEY + SEPARATOR + + Integer.toString(config.userApproved) + NL); + out.writeUTF(SHARED_KEY + SEPARATOR + Boolean.toString(config.shared) + NL); + String allowedKeyManagementString = + makeString(config.allowedKeyManagement, + WifiConfiguration.KeyMgmt.strings); + out.writeUTF(AUTH_KEY + SEPARATOR + + allowedKeyManagementString + NL); + out.writeUTF(NETWORK_SELECTION_STATUS_KEY + SEPARATOR + + status.getNetworkSelectionStatus() + NL); + out.writeUTF(NETWORK_SELECTION_DISABLE_REASON_KEY + SEPARATOR + + status.getNetworkSelectionDisableReason() + NL); + + if (status.getConnectChoice() != null) { + out.writeUTF(CHOICE_KEY + SEPARATOR + status.getConnectChoice() + NL); + out.writeUTF(CHOICE_TIME_KEY + SEPARATOR + + status.getConnectChoiceTimestamp() + NL); + } + + if (config.linkedConfigurations != null) { + log("writeKnownNetworkHistory write linked " + + config.linkedConfigurations.size()); + + for (String key : config.linkedConfigurations.keySet()) { + out.writeUTF(LINK_KEY + SEPARATOR + key + NL); + } + } + + String macAddress = config.defaultGwMacAddress; + if (macAddress != null) { + out.writeUTF(DEFAULT_GW_KEY + SEPARATOR + macAddress + NL); + } + + if (getScanDetailCache(config, scanDetailCaches) != null) { + for (ScanDetail scanDetail : getScanDetailCache(config, + scanDetailCaches).values()) { + ScanResult result = scanDetail.getScanResult(); + out.writeUTF(BSSID_KEY + SEPARATOR + + result.BSSID + NL); + out.writeUTF(FREQ_KEY + SEPARATOR + + Integer.toString(result.frequency) + NL); + + out.writeUTF(RSSI_KEY + SEPARATOR + + Integer.toString(result.level) + NL); + + out.writeUTF(BSSID_KEY_END + NL); + } + } + out.writeUTF(HAS_EVER_CONNECTED_KEY + SEPARATOR + + Boolean.toString(status.getHasEverConnected()) + NL); + out.writeUTF(NL); + // Add extra blank lines for clarity + out.writeUTF(NL); + out.writeUTF(NL); + } + if (deletedEphemeralSSIDs != null && deletedEphemeralSSIDs.size() > 0) { + for (String ssid : deletedEphemeralSSIDs) { + out.writeUTF(DELETED_EPHEMERAL_KEY); + out.writeUTF(ssid); + out.writeUTF(NL); + } + } + } + }); + } + + /** + * Adds information stored in networkHistory.txt to the given configs. The configs are provided + * as a mapping from configKey to WifiConfiguration, because the WifiConfigurations themselves + * do not contain sufficient information to compute their configKeys until after the information + * that is stored in networkHistory.txt has been added to them. + * + * @param configs mapping from configKey to a WifiConfiguration that contains the information + * information read from wpa_supplicant.conf + */ + public void readNetworkHistory(Map configs, + Map scanDetailCaches, + Set deletedEphemeralSSIDs) { + + try (DataInputStream in = + new DataInputStream(new BufferedInputStream( + new FileInputStream(NETWORK_HISTORY_CONFIG_FILE)))) { + + String bssid = null; + String ssid = null; + + int freq = 0; + int status = 0; + long seen = 0; + int rssi = WifiConfiguration.INVALID_RSSI; + String caps = null; + + WifiConfiguration config = null; + while (true) { + String line = in.readUTF(); + if (line == null) { + break; + } + int colon = line.indexOf(':'); + if (colon < 0) { + continue; + } + + String key = line.substring(0, colon).trim(); + String value = line.substring(colon + 1).trim(); + + if (key.equals(CONFIG_KEY)) { + config = configs.get(value); + + // skip reading that configuration data + // since we don't have a corresponding network ID + if (config == null) { + Log.e(TAG, "readNetworkHistory didnt find netid for hash=" + + Integer.toString(value.hashCode()) + + " key: " + value); + mLostConfigsDbg.add(value); + continue; + } else { + // After an upgrade count old connections as owned by system + if (config.creatorName == null || config.lastUpdateName == null) { + config.creatorName = + mContext.getPackageManager().getNameForUid(Process.SYSTEM_UID); + config.lastUpdateName = config.creatorName; + + if (DBG) { + Log.w(TAG, "Upgrading network " + config.networkId + + " to " + config.creatorName); + } + } + } + } else if (config != null) { + NetworkSelectionStatus networkStatus = config.getNetworkSelectionStatus(); + switch (key) { + case SSID_KEY: + if (config.isPasspoint()) { + break; + } + ssid = value; + if (config.SSID != null && !config.SSID.equals(ssid)) { + loge("Error parsing network history file, mismatched SSIDs"); + config = null; //error + ssid = null; + } else { + config.SSID = ssid; + } + break; + case CONFIG_BSSID_KEY: + config.BSSID = value.equals("null") ? null : value; + break; + case FQDN_KEY: + // Check for literal 'null' to be backwards compatible. + config.FQDN = value.equals("null") ? null : value; + break; + case DEFAULT_GW_KEY: + config.defaultGwMacAddress = value; + break; + case SELF_ADDED_KEY: + config.selfAdded = Boolean.parseBoolean(value); + break; + case DID_SELF_ADD_KEY: + config.didSelfAdd = Boolean.parseBoolean(value); + break; + case NO_INTERNET_ACCESS_REPORTS_KEY: + config.numNoInternetAccessReports = Integer.parseInt(value); + break; + case VALIDATED_INTERNET_ACCESS_KEY: + config.validatedInternetAccess = Boolean.parseBoolean(value); + break; + case NO_INTERNET_ACCESS_EXPECTED_KEY: + config.noInternetAccessExpected = Boolean.parseBoolean(value); + break; + case CREATION_TIME_KEY: + config.creationTime = value; + break; + case UPDATE_TIME_KEY: + config.updateTime = value; + break; + case EPHEMERAL_KEY: + config.ephemeral = Boolean.parseBoolean(value); + break; + case METERED_HINT_KEY: + config.meteredHint = Boolean.parseBoolean(value); + break; + case METERED_OVERRIDE_KEY: + config.meteredOverride = Integer.parseInt(value); + break; + case USE_EXTERNAL_SCORES_KEY: + config.useExternalScores = Boolean.parseBoolean(value); + break; + case CREATOR_UID_KEY: + config.creatorUid = Integer.parseInt(value); + break; + case SCORER_OVERRIDE_KEY: + config.numScorerOverride = Integer.parseInt(value); + break; + case SCORER_OVERRIDE_AND_SWITCH_KEY: + config.numScorerOverrideAndSwitchedNetwork = Integer.parseInt(value); + break; + case NUM_ASSOCIATION_KEY: + config.numAssociation = Integer.parseInt(value); + break; + case CONNECT_UID_KEY: + config.lastConnectUid = Integer.parseInt(value); + break; + case UPDATE_UID_KEY: + config.lastUpdateUid = Integer.parseInt(value); + break; + case PEER_CONFIGURATION_KEY: + config.peerWifiConfiguration = value; + break; + case NETWORK_SELECTION_STATUS_KEY: + int networkStatusValue = Integer.parseInt(value); + // Reset temporarily disabled network status + if (networkStatusValue == + NetworkSelectionStatus.NETWORK_SELECTION_TEMPORARY_DISABLED) { + networkStatusValue = + NetworkSelectionStatus.NETWORK_SELECTION_ENABLED; + } + networkStatus.setNetworkSelectionStatus(networkStatusValue); + break; + case NETWORK_SELECTION_DISABLE_REASON_KEY: + networkStatus.setNetworkSelectionDisableReason(Integer.parseInt(value)); + break; + case CHOICE_KEY: + networkStatus.setConnectChoice(value); + break; + case CHOICE_TIME_KEY: + networkStatus.setConnectChoiceTimestamp(Long.parseLong(value)); + break; + case LINK_KEY: + if (config.linkedConfigurations == null) { + config.linkedConfigurations = new HashMap<>(); + } else { + config.linkedConfigurations.put(value, -1); + } + break; + case BSSID_KEY: + status = 0; + ssid = null; + bssid = null; + freq = 0; + seen = 0; + rssi = WifiConfiguration.INVALID_RSSI; + caps = ""; + break; + case RSSI_KEY: + rssi = Integer.parseInt(value); + break; + case FREQ_KEY: + freq = Integer.parseInt(value); + break; + case DATE_KEY: + /* + * when reading the configuration from file we don't update the date + * so as to avoid reading back stale or non-sensical data that would + * depend on network time. + * The date of a WifiConfiguration should only come from actual scan + * result. + * + String s = key.replace(FREQ_KEY, ""); + seen = Integer.getInteger(s); + */ + break; + case BSSID_KEY_END: + if ((bssid != null) && (ssid != null)) { + if (getScanDetailCache(config, scanDetailCaches) != null) { + WifiSsid wssid = WifiSsid.createFromAsciiEncoded(ssid); + ScanDetail scanDetail = new ScanDetail(wssid, bssid, + caps, rssi, freq, (long) 0, seen); + getScanDetailCache(config, scanDetailCaches).put(scanDetail); + } + } + break; + case DELETED_EPHEMERAL_KEY: + if (!TextUtils.isEmpty(value)) { + deletedEphemeralSSIDs.add(value); + } + break; + case CREATOR_NAME_KEY: + config.creatorName = value; + break; + case UPDATE_NAME_KEY: + config.lastUpdateName = value; + break; + case USER_APPROVED_KEY: + config.userApproved = Integer.parseInt(value); + break; + case SHARED_KEY: + config.shared = Boolean.parseBoolean(value); + break; + case HAS_EVER_CONNECTED_KEY: + networkStatus.setHasEverConnected(Boolean.parseBoolean(value)); + break; + } + } + } + } catch (EOFException e) { + // do nothing + } catch (FileNotFoundException e) { + Log.i(TAG, "readNetworkHistory: no config file, " + e); + } catch (NumberFormatException e) { + Log.e(TAG, "readNetworkHistory: failed to parse, " + e, e); + } catch (IOException e) { + Log.e(TAG, "readNetworkHistory: failed to read, " + e, e); + } + } + + /** + * Ported this out of WifiServiceImpl, I have no idea what it's doing + * figure out what/why this is doing + * Port it into WifiConfiguration, then remove all the silly business from ServiceImpl + */ + public boolean isValid(WifiConfiguration config) { + if (config.allowedKeyManagement == null) { + return false; + } + if (config.allowedKeyManagement.cardinality() > 1) { + if (config.allowedKeyManagement.cardinality() != 2) { + return false; + } + if (!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_EAP)) { + return false; + } + if ((!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.IEEE8021X)) + && (!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_PSK))) { + return false; + } + } + return true; + } + + private static String makeString(BitSet set, String[] strings) { + StringBuffer buf = new StringBuffer(); + int nextSetBit = -1; + + /* Make sure all set bits are in [0, strings.length) to avoid + * going out of bounds on strings. (Shouldn't happen, but...) */ + set = set.get(0, strings.length); + + while ((nextSetBit = set.nextSetBit(nextSetBit + 1)) != -1) { + buf.append(strings[nextSetBit].replace('_', '-')).append(' '); + } + + // remove trailing space + if (set.cardinality() > 0) { + buf.setLength(buf.length() - 1); + } + + return buf.toString(); + } + + protected void logv(String s) { + Log.v(TAG, s); + } + protected void logd(String s) { + Log.d(TAG, s); + } + protected void log(String s) { + Log.d(TAG, s); + } + protected void loge(String s) { + loge(s, false); + } + protected void loge(String s, boolean stack) { + if (stack) { + Log.e(TAG, s + " stack:" + Thread.currentThread().getStackTrace()[2].getMethodName() + + " - " + Thread.currentThread().getStackTrace()[3].getMethodName() + + " - " + Thread.currentThread().getStackTrace()[4].getMethodName() + + " - " + Thread.currentThread().getStackTrace()[5].getMethodName()); + } else { + Log.e(TAG, s); + } + } + + private ScanDetailCache getScanDetailCache(WifiConfiguration config, + Map scanDetailCaches) { + if (config == null || scanDetailCaches == null) return null; + ScanDetailCache cache = scanDetailCaches.get(config.networkId); + if (cache == null && config.networkId != WifiConfiguration.INVALID_NETWORK_ID) { + cache = + new ScanDetailCache( + config, WifiConfigManager.SCAN_CACHE_ENTRIES_MAX_SIZE, + WifiConfigManager.SCAN_CACHE_ENTRIES_TRIM_SIZE); + scanDetailCaches.put(config.networkId, cache); + } + return cache; + } +} diff --git a/service/java/com/android/server/wifi/WifiStateMachine.java b/service/java/com/android/server/wifi/WifiStateMachine.java index ac7d748bb..44d6339c6 100644 --- a/service/java/com/android/server/wifi/WifiStateMachine.java +++ b/service/java/com/android/server/wifi/WifiStateMachine.java @@ -4278,6 +4278,10 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss mLastSignalLevel = -1; mWifiInfo.setMacAddress(mWifiNative.getMacAddress()); + // Attempt to migrate data out of legacy store. + if (!mWifiConfigManager.migrateFromLegacyStore()) { + Log.e(TAG, "Failed to migrate from legacy config store"); + } initializeWpsDetails(); sendSupplicantConnectionChangedBroadcast(true); transitionTo(mSupplicantStartedState); diff --git a/service/java/com/android/server/wifi/hotspot2/LegacyPasspointConfigParser.java b/service/java/com/android/server/wifi/hotspot2/LegacyPasspointConfigParser.java new file mode 100644 index 000000000..31795f126 --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/LegacyPasspointConfigParser.java @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2017 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.wifi.hotspot2; + +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Utility class for parsing legacy (N and older) Passpoint configuration file content + * (/data/misc/wifi/PerProviderSubscription.conf). In N and older, only Release 1 is supported. + * + * This class only retrieve the relevant Release 1 configuration fields that are not backed + * elsewhere. Below are relevant fields: + * - FQDN (used for linking with configuration data stored elsewhere) + * - Friendly Name + * - Roaming Consortium + * - Realm + * - IMSI (for SIM credential) + * + * Below is an example content of a Passpoint configuration file: + * + * tree 3:1.2(urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0) + * 8:MgmtTree+ + * 17:PerProviderSubscription+ + * 4:r1i1+ + * 6:HomeSP+ + * c:FriendlyName=d:Test Provider + * 4:FQDN=8:test.net + * 13:RoamingConsortiumOI=9:1234,5678 + * . + * a:Credential+ + * 10:UsernamePassword+ + * 8:Username=4:user + * 8:Password=4:pass + * + * 9:EAPMethod+ + * 7:EAPType=2:21 + * b:InnerMethod=3:PAP + * . + * . + * 5:Realm=a:boingo.com + * . + * . + * . + * . + * + * Each string is prefixed with a "|StringBytesInHex|:". + * '+' indicates start of a new internal node. + * '.' indicates end of the current internal node. + * '=' indicates "value" of a leaf node. + * + */ +public class LegacyPasspointConfigParser { + private static final String TAG = "LegacyPasspointConfigParser"; + + private static final String TAG_MANAGEMENT_TREE = "MgmtTree"; + private static final String TAG_PER_PROVIDER_SUBSCRIPTION = "PerProviderSubscription"; + private static final String TAG_HOMESP = "HomeSP"; + private static final String TAG_FQDN = "FQDN"; + private static final String TAG_FRIENDLY_NAME = "FriendlyName"; + private static final String TAG_ROAMING_CONSORTIUM_OI = "RoamingConsortiumOI"; + private static final String TAG_CREDENTIAL = "Credential"; + private static final String TAG_REALM = "Realm"; + private static final String TAG_SIM = "SIM"; + private static final String TAG_IMSI = "IMSI"; + + private static final String LONG_ARRAY_SEPARATOR = ","; + private static final String END_OF_INTERNAL_NODE_INDICATOR = "."; + private static final char START_OF_INTERNAL_NODE_INDICATOR = '+'; + private static final char STRING_PREFIX_INDICATOR = ':'; + private static final char STRING_VALUE_INDICATOR = '='; + + /** + * An abstraction for a node within a tree. A node can be an internal node (contained + * children nodes) or a leaf node (contained a String value). + */ + private abstract static class Node { + private final String mName; + Node(String name) { + mName = name; + } + + /** + * @return the name of the node + */ + public String getName() { + return mName; + } + + /** + * Applies for internal node only. + * + * @return the list of children nodes. + */ + public abstract List getChildren(); + + /** + * Applies for leaf node only. + * + * @return the string value of the node + */ + public abstract String getValue(); + } + + /** + * Class representing an internal node of a tree. It contained a list of child nodes. + */ + private static class InternalNode extends Node { + private final List mChildren; + InternalNode(String name, List children) { + super(name); + mChildren = children; + } + + @Override + public List getChildren() { + return mChildren; + } + + @Override + public String getValue() { + return null; + } + } + + /** + * Class representing a leaf node of a tree. It contained a String type value. + */ + private static class LeafNode extends Node { + private final String mValue; + LeafNode(String name, String value) { + super(name); + mValue = value; + } + + @Override + public List getChildren() { + return null; + } + + @Override + public String getValue() { + return mValue; + } + } + + public LegacyPasspointConfigParser() {} + + /** + * Parse the legacy Passpoint configuration file content, only retrieve the relevant + * configurations that are not saved elsewhere. + * + * For both N and M, only Release 1 is supported. Most of the configurations are saved + * elsewhere as part of the {@link android.net.wifi.WifiConfiguration} data. + * The configurations needed from the legacy Passpoint configuration file are: + * + * - FQDN - needed to be able to link to the associated {@link WifiConfiguration} data + * - Friendly Name + * - Roaming Consortium OIs + * - Realm + * - IMSI (for SIM credential) + * + * Make this function non-static so that it can be mocked during unit test. + * + * @param fileName The file name of the configuration file + * @return Map of FQDN to {@link LegacyPasspointConfig} + * @throws IOException + */ + public Map parseConfig(String fileName) + throws IOException { + Map configs = new HashMap<>(); + BufferedReader in = new BufferedReader(new FileReader(fileName)); + in.readLine(); // Ignore the first line which contained the header. + + // Convert the configuration data to a management tree represented by a root {@link Node}. + Node root = buildNode(in); + + if (root == null || root.getChildren() == null) { + Log.d(TAG, "Empty configuration data"); + return configs; + } + + // Verify root node name. + if (!TextUtils.equals(TAG_MANAGEMENT_TREE, root.getName())) { + throw new IOException("Unexpected root node: " + root.getName()); + } + + // Process and retrieve the configuration from each PPS (PerProviderSubscription) node. + List ppsNodes = root.getChildren(); + for (Node ppsNode : ppsNodes) { + LegacyPasspointConfig config = processPpsNode(ppsNode); + configs.put(config.mFqdn, config); + } + return configs; + } + + /** + * Build a {@link Node} from the current line in the buffer. A node can be an internal + * node (ends with '+') or a leaf node. + * + * @param in Input buffer to read data from + * @return {@link Node} representing the current line + * @throws IOException + */ + private static Node buildNode(BufferedReader in) throws IOException { + // Read until non-empty line. + String currentLine = null; + while ((currentLine = in.readLine()) != null) { + if (!currentLine.isEmpty()) { + break; + } + } + + // Return null if EOF is reached. + if (currentLine == null) { + return null; + } + + // Remove the leading and the trailing whitespaces. + currentLine = currentLine.trim(); + + // Check for the internal node terminator. + if (TextUtils.equals(END_OF_INTERNAL_NODE_INDICATOR, currentLine)) { + return null; + } + + // Parse the name-value of the current line. The value will be null if the current line + // is not a leaf node (e.g. line ends with a '+'). + // Each line is encoded in UTF-8. + Pair nameValuePair = + parseLine(currentLine.getBytes(StandardCharsets.UTF_8)); + if (nameValuePair.second != null) { + return new LeafNode(nameValuePair.first, nameValuePair.second); + } + + // Parse the children contained under this internal node. + List children = new ArrayList<>(); + Node child = null; + while ((child = buildNode(in)) != null) { + children.add(child); + } + return new InternalNode(nameValuePair.first, children); + } + + /** + * Process a PPS (PerProviderSubscription) node to retrieve Passpoint configuration data. + * + * @param ppsNode The PPS node to process + * @return {@link LegacyPasspointConfig} + * @throws IOException + */ + private static LegacyPasspointConfig processPpsNode(Node ppsNode) throws IOException { + if (ppsNode.getChildren() == null || ppsNode.getChildren().size() != 1) { + throw new IOException("PerProviderSubscription node should contain " + + "one instance node"); + } + + if (!TextUtils.equals(TAG_PER_PROVIDER_SUBSCRIPTION, ppsNode.getName())) { + throw new IOException("Unexpected name for PPS node: " + ppsNode.getName()); + } + + // Retrieve the PPS instance node. + Node instanceNode = ppsNode.getChildren().get(0); + if (instanceNode.getChildren() == null) { + throw new IOException("PPS instance node doesn't contained any children"); + } + + // Process and retrieve the relevant configurations under the PPS instance node. + LegacyPasspointConfig config = new LegacyPasspointConfig(); + for (Node node : instanceNode.getChildren()) { + switch (node.getName()) { + case TAG_HOMESP: + processHomeSPNode(node, config); + break; + case TAG_CREDENTIAL: + processCredentialNode(node, config); + break; + default: + Log.d(TAG, "Ignore uninterested field under PPS instance: " + node.getName()); + break; + } + } + if (config.mFqdn == null) { + throw new IOException("PPS instance missing FQDN"); + } + return config; + } + + /** + * Process a HomeSP node to retrieve configuration data into the given |config|. + * + * @param homeSpNode The HomeSP node to process + * @param config The config object to fill in the data + * @throws IOException + */ + private static void processHomeSPNode(Node homeSpNode, LegacyPasspointConfig config) + throws IOException { + if (homeSpNode.getChildren() == null) { + throw new IOException("HomeSP node should contain at least one child node"); + } + + for (Node node : homeSpNode.getChildren()) { + switch (node.getName()) { + case TAG_FQDN: + config.mFqdn = getValue(node); + break; + case TAG_FRIENDLY_NAME: + config.mFriendlyName = getValue(node); + break; + case TAG_ROAMING_CONSORTIUM_OI: + config.mRoamingConsortiumOis = parseLongArray(getValue(node)); + break; + default: + Log.d(TAG, "Ignore uninterested field under HomeSP: " + node.getName()); + break; + } + } + } + + /** + * Process a Credential node to retrieve configuration data into the given |config|. + * + * @param credentialNode The Credential node to process + * @param config The config object to fill in the data + * @throws IOException + */ + private static void processCredentialNode(Node credentialNode, + LegacyPasspointConfig config) + throws IOException { + if (credentialNode.getChildren() == null) { + throw new IOException("Credential node should contain at least one child node"); + } + + for (Node node : credentialNode.getChildren()) { + switch (node.getName()) { + case TAG_REALM: + config.mRealm = getValue(node); + break; + case TAG_SIM: + processSimNode(node, config); + break; + default: + Log.d(TAG, "Ignore uninterested field under Credential: " + node.getName()); + break; + } + } + } + + /** + * Process a SIM node to retrieve configuration data into the given |config|. + * + * @param simNode The SIM node to process + * @param config The config object to fill in the data + * @throws IOException + */ + private static void processSimNode(Node simNode, LegacyPasspointConfig config) + throws IOException { + if (simNode.getChildren() == null) { + throw new IOException("SIM node should contain at least one child node"); + } + + for (Node node : simNode.getChildren()) { + switch (node.getName()) { + case TAG_IMSI: + config.mImsi = getValue(node); + break; + default: + Log.d(TAG, "Ignore uninterested field under SIM: " + node.getName()); + break; + } + } + } + + /** + * Parse the given line in the legacy Passpoint configuration file. + * A line can be in the following formats: + * 2:ab+ // internal node + * 2:ab=2:bc // leaf node + * . // end of internal node + * + * @param line The line to parse + * @return name-value pair, a value of null indicates internal node + * @throws IOException + */ + private static Pair parseLine(byte[] lineBytes) throws IOException { + Pair nameIndexPair = parseString(lineBytes, 0); + int currentIndex = nameIndexPair.second; + try { + if (lineBytes[currentIndex] == START_OF_INTERNAL_NODE_INDICATOR) { + return Pair.create(nameIndexPair.first, null); + } + + if (lineBytes[currentIndex] != STRING_VALUE_INDICATOR) { + throw new IOException("Invalid line - missing both node and value indicator: " + + new String(lineBytes, StandardCharsets.UTF_8)); + } + } catch (IndexOutOfBoundsException e) { + throw new IOException("Invalid line - " + e.getMessage() + ": " + + new String(lineBytes, StandardCharsets.UTF_8)); + } + Pair valueIndexPair = parseString(lineBytes, currentIndex + 1); + return Pair.create(nameIndexPair.first, valueIndexPair.first); + } + + /** + * Parse a string value in the given line from the given start index. + * A string value is in the following format: + * |HexByteLength|:|String| + * + * The length value indicates the number of UTF-8 bytes in hex for the given string. + * + * For example: 3:abc + * + * @param lineBytes The UTF-8 bytes of the line to parse + * @param startIndex The start index from the given line to parse from + * @return Pair of a string value and an index pointed to character after the string value + * @throws IOException + */ + private static Pair parseString(byte[] lineBytes, int startIndex) + throws IOException { + // Locate the index that separate length and the string value. + int prefixIndex = -1; + for (int i = startIndex; i < lineBytes.length; i++) { + if (lineBytes[i] == STRING_PREFIX_INDICATOR) { + prefixIndex = i; + break; + } + } + if (prefixIndex == -1) { + throw new IOException("Invalid line - missing string prefix: " + + new String(lineBytes, StandardCharsets.UTF_8)); + } + + try { + String lengthStr = new String(lineBytes, startIndex, prefixIndex - startIndex, + StandardCharsets.UTF_8); + int length = Integer.parseInt(lengthStr, 16); + int strStartIndex = prefixIndex + 1; + // The length might account for bytes for the whitespaces, since the whitespaces are + // already trimmed, ignore them. + if ((strStartIndex + length) > lineBytes.length) { + length = lineBytes.length - strStartIndex; + } + return Pair.create( + new String(lineBytes, strStartIndex, length, StandardCharsets.UTF_8), + strStartIndex + length); + } catch (NumberFormatException | IndexOutOfBoundsException e) { + throw new IOException("Invalid line - " + e.getMessage() + ": " + + new String(lineBytes, StandardCharsets.UTF_8)); + } + } + + /** + * Parse a long array from the given string. + * + * @param str The string to parse + * @return long[] + * @throws IOException + */ + private static long[] parseLongArray(String str) + throws IOException { + String[] strArray = str.split(LONG_ARRAY_SEPARATOR); + long[] longArray = new long[strArray.length]; + for (int i = 0; i < longArray.length; i++) { + try { + longArray[i] = Long.parseLong(strArray[i], 16); + } catch (NumberFormatException e) { + throw new IOException("Invalid long integer value: " + strArray[i]); + } + } + return longArray; + } + + /** + * Get the String value of the given node. An IOException will be thrown if the given + * node doesn't contain a String value (internal node). + * + * @param node The node to get the value from + * @return String + * @throws IOException + */ + private static String getValue(Node node) throws IOException { + if (node.getValue() == null) { + throw new IOException("Attempt to retreive value from non-leaf node: " + + node.getName()); + } + return node.getValue(); + } +} -- cgit v1.2.3