/** * 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 android.net.wifi.hotspot2.omadm; import android.net.wifi.hotspot2.PasspointConfiguration; import android.net.wifi.hotspot2.pps.Credential; import android.net.wifi.hotspot2.pps.HomeSp; import android.net.wifi.hotspot2.pps.Policy; import android.net.wifi.hotspot2.pps.UpdateParameter; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import java.io.IOException; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.xml.sax.SAXException; /** * Utility class for converting OMA-DM (Open Mobile Alliance's Device Management) * PPS-MO (PerProviderSubscription Management Object) XML tree to a * {@link PasspointConfiguration} object. * * Currently this only supports PerProviderSubscription/HomeSP and * PerProviderSubscription/Credential subtree for Hotspot 2.0 Release 1 support. * * For more info, refer to Hotspot 2.0 PPS MO defined in section 9.1 of the Hotspot 2.0 * Release 2 Technical Specification. * * Below is a sample XML string for a Release 1 PPS MO tree: * * * 1.2 * * PerProviderSubscription * * * urn:wfa:mo:hotspot2dot0­perprovidersubscription:1.0 * * * * i001 * * HomeSP * * FriendlyName * Century House * * * FQDN * mi6.co.uk * * * RoamingConsortiumOI * 112233,445566 * * * * Credential * * Realm * shaken.stirred.com * * * UsernamePassword * * Username * james * * * Password * Ym9uZDAwNw== * * * EAPMethod * * EAPType * 21 * * * InnerMethod * MS-CHAP-V2 * * * * * * * */ public final class PpsMoParser { private static final String TAG = "PpsMoParser"; /** * XML tags expected in the PPS MO (PerProviderSubscription Management Object) XML tree. */ private static final String TAG_MANAGEMENT_TREE = "MgmtTree"; private static final String TAG_VER_DTD = "VerDTD"; private static final String TAG_NODE = "Node"; private static final String TAG_NODE_NAME = "NodeName"; private static final String TAG_RT_PROPERTIES = "RTProperties"; private static final String TAG_TYPE = "Type"; private static final String TAG_DDF_NAME = "DDFName"; private static final String TAG_VALUE = "Value"; /** * Name for PerProviderSubscription node. */ private static final String NODE_PER_PROVIDER_SUBSCRIPTION = "PerProviderSubscription"; /** * Fields under PerProviderSubscription. */ private static final String NODE_UPDATE_IDENTIFIER = "UpdateIdentifier"; private static final String NODE_AAA_SERVER_TRUST_ROOT = "AAAServerTrustRoot"; private static final String NODE_SUBSCRIPTION_UPDATE = "SubscriptionUpdate"; private static final String NODE_SUBSCRIPTION_PARAMETER = "SubscriptionParameter"; private static final String NODE_TYPE_OF_SUBSCRIPTION = "TypeOfSubscription"; private static final String NODE_USAGE_LIMITS = "UsageLimits"; private static final String NODE_DATA_LIMIT = "DataLimit"; private static final String NODE_START_DATE = "StartDate"; private static final String NODE_TIME_LIMIT = "TimeLimit"; private static final String NODE_USAGE_TIME_PERIOD = "UsageTimePeriod"; private static final String NODE_CREDENTIAL_PRIORITY = "CredentialPriority"; private static final String NODE_EXTENSION = "Extension"; /** * Fields under HomeSP subtree. */ private static final String NODE_HOMESP = "HomeSP"; private static final String NODE_FQDN = "FQDN"; private static final String NODE_FRIENDLY_NAME = "FriendlyName"; private static final String NODE_ROAMING_CONSORTIUM_OI = "RoamingConsortiumOI"; private static final String NODE_NETWORK_ID = "NetworkID"; private static final String NODE_SSID = "SSID"; private static final String NODE_HESSID = "HESSID"; private static final String NODE_ICON_URL = "IconURL"; private static final String NODE_HOME_OI_LIST = "HomeOIList"; private static final String NODE_HOME_OI = "HomeOI"; private static final String NODE_HOME_OI_REQUIRED = "HomeOIRequired"; private static final String NODE_OTHER_HOME_PARTNERS = "OtherHomePartners"; /** * Fields under Credential subtree. */ private static final String NODE_CREDENTIAL = "Credential"; private static final String NODE_CREATION_DATE = "CreationDate"; private static final String NODE_EXPIRATION_DATE = "ExpirationDate"; private static final String NODE_USERNAME_PASSWORD = "UsernamePassword"; private static final String NODE_USERNAME = "Username"; private static final String NODE_PASSWORD = "Password"; private static final String NODE_MACHINE_MANAGED = "MachineManaged"; private static final String NODE_SOFT_TOKEN_APP = "SoftTokenApp"; private static final String NODE_ABLE_TO_SHARE = "AbleToShare"; private static final String NODE_EAP_METHOD = "EAPMethod"; private static final String NODE_EAP_TYPE = "EAPType"; private static final String NODE_VENDOR_ID = "VendorId"; private static final String NODE_VENDOR_TYPE = "VendorType"; private static final String NODE_INNER_EAP_TYPE = "InnerEAPType"; private static final String NODE_INNER_VENDOR_ID = "InnerVendorID"; private static final String NODE_INNER_VENDOR_TYPE = "InnerVendorType"; private static final String NODE_INNER_METHOD = "InnerMethod"; private static final String NODE_DIGITAL_CERTIFICATE = "DigitalCertificate"; private static final String NODE_CERTIFICATE_TYPE = "CertificateType"; private static final String NODE_CERT_SHA256_FINGERPRINT = "CertSHA256Fingerprint"; private static final String NODE_REALM = "Realm"; private static final String NODE_SIM = "SIM"; private static final String NODE_SIM_IMSI = "IMSI"; private static final String NODE_CHECK_AAA_SERVER_CERT_STATUS = "CheckAAAServerCertStatus"; /** * Fields under Policy subtree. */ private static final String NODE_POLICY = "Policy"; private static final String NODE_PREFERRED_ROAMING_PARTNER_LIST = "PreferredRoamingPartnerList"; private static final String NODE_FQDN_MATCH = "FQDN_Match"; private static final String NODE_PRIORITY = "Priority"; private static final String NODE_COUNTRY = "Country"; private static final String NODE_MIN_BACKHAUL_THRESHOLD = "MinBackhaulThreshold"; private static final String NODE_NETWORK_TYPE = "NetworkType"; private static final String NODE_DOWNLINK_BANDWIDTH = "DLBandwidth"; private static final String NODE_UPLINK_BANDWIDTH = "ULBandwidth"; private static final String NODE_POLICY_UPDATE = "PolicyUpdate"; private static final String NODE_UPDATE_INTERVAL = "UpdateInterval"; private static final String NODE_UPDATE_METHOD = "UpdateMethod"; private static final String NODE_RESTRICTION = "Restriction"; private static final String NODE_URI = "URI"; private static final String NODE_TRUST_ROOT = "TrustRoot"; private static final String NODE_CERT_URL = "CertURL"; private static final String NODE_SP_EXCLUSION_LIST = "SPExclusionList"; private static final String NODE_REQUIRED_PROTO_PORT_TUPLE = "RequiredProtoPortTuple"; private static final String NODE_IP_PROTOCOL = "IPProtocol"; private static final String NODE_PORT_NUMBER = "PortNumber"; private static final String NODE_MAXIMUM_BSS_LOAD_VALUE = "MaximumBSSLoadValue"; private static final String NODE_OTHER = "Other"; /** * URN (Unique Resource Name) for PerProviderSubscription Management Object Tree. */ private static final String PPS_MO_URN = "urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0"; /** * Exception for generic parsing errors. */ private static class ParsingException extends Exception { public ParsingException(String message) { super(message); } } /** * Class representing a node within the PerProviderSubscription tree. * This is used to flatten out and eliminate the extra layering in the XMLNode tree, * to make the data parsing easier and cleaner. * * A PPSNode can be an internal or a leaf node, but not both. * */ private static abstract class PPSNode { private final String mName; public PPSNode(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(); /** * @return a flag indicating if this is a leaf or an internal node */ public abstract boolean isLeaf(); } /** * Class representing a leaf node in a PPS (PerProviderSubscription) tree. */ private static class LeafNode extends PPSNode { private final String mValue; public LeafNode(String nodeName, String value) { super(nodeName); mValue = value; } @Override public String getValue() { return mValue; } @Override public List getChildren() { return null; } @Override public boolean isLeaf() { return true; } } /** * Class representing an internal node in a PPS (PerProviderSubscription) tree. */ private static class InternalNode extends PPSNode { private final List mChildren; public InternalNode(String nodeName, List children) { super(nodeName); mChildren = children; } @Override public String getValue() { return null; } @Override public List getChildren() { return mChildren; } @Override public boolean isLeaf() { return false; } } /** * @hide */ public PpsMoParser() {} /** * Convert a XML string representation of a PPS MO (PerProviderSubscription * Management Object) tree to a {@link PasspointConfiguration} object. * * @param xmlString XML string representation of a PPS MO tree * @return {@link PasspointConfiguration} or null */ public static PasspointConfiguration parseMoText(String xmlString) { // Convert the XML string to a XML tree. XMLParser xmlParser = new XMLParser(); XMLNode root = null; try { root = xmlParser.parse(xmlString); } catch(IOException | SAXException e) { return null; } if (root == null) { return null; } // Verify root node is a "MgmtTree" node. if (root.getTag() != TAG_MANAGEMENT_TREE) { Log.e(TAG, "Root is not a MgmtTree"); return null; } String verDtd = null; // Used for detecting duplicate VerDTD element. PasspointConfiguration config = null; for (XMLNode child : root.getChildren()) { switch(child.getTag()) { case TAG_VER_DTD: if (verDtd != null) { Log.e(TAG, "Duplicate VerDTD element"); return null; } verDtd = child.getText(); break; case TAG_NODE: if (config != null) { Log.e(TAG, "Unexpected multiple Node element under MgmtTree"); return null; } try { config = parsePpsNode(child); } catch (ParsingException e) { Log.e(TAG, e.getMessage()); return null; } break; default: Log.e(TAG, "Unknown node: " + child.getTag()); return null; } } return config; } /** * Parse a PerProviderSubscription node. Below is the format of the XML tree (with * each XML element represent a node in the tree): * * * PerProviderSubscription * * ... * * * UpdateIdentifier * ... * * * ... * * * * @param node XMLNode that contains PerProviderSubscription node. * @return PasspointConfiguration or null * @throws ParsingException */ private static PasspointConfiguration parsePpsNode(XMLNode node) throws ParsingException { PasspointConfiguration config = null; String nodeName = null; int updateIdentifier = Integer.MIN_VALUE; for (XMLNode child : node.getChildren()) { switch (child.getTag()) { case TAG_NODE_NAME: if (nodeName != null) { throw new ParsingException("Duplicate NodeName: " + child.getText()); } nodeName = child.getText(); if (!TextUtils.equals(nodeName, NODE_PER_PROVIDER_SUBSCRIPTION)) { throw new ParsingException("Unexpected NodeName: " + nodeName); } break; case TAG_NODE: // A node can be either an UpdateIdentifier node or a PerProviderSubscription // instance node. Flatten out the XML tree first by converting it to a PPS // tree to reduce the complexity of the parsing code. PPSNode ppsNodeRoot = buildPpsNode(child); if (TextUtils.equals(ppsNodeRoot.getName(), NODE_UPDATE_IDENTIFIER)) { if (updateIdentifier != Integer.MIN_VALUE) { throw new ParsingException("Multiple node for UpdateIdentifier"); } updateIdentifier = parseInteger(getPpsNodeValue(ppsNodeRoot)); } else { // Only one PerProviderSubscription instance is expected and allowed. if (config != null) { throw new ParsingException("Multiple PPS instance"); } config = parsePpsInstance(ppsNodeRoot); } break; case TAG_RT_PROPERTIES: // Parse and verify URN stored in the RT (Run Time) Properties. String urn = parseUrn(child); if (!TextUtils.equals(urn, PPS_MO_URN)) { throw new ParsingException("Unknown URN: " + urn); } break; default: throw new ParsingException("Unknown tag under PPS node: " + child.getTag()); } } if (config != null && updateIdentifier != Integer.MIN_VALUE) { config.setUpdateIdentifier(updateIdentifier); } return config; } /** * Parse the URN stored in the RTProperties. Below is the format of the RTPProperties node: * * * * urn:... * * * * @param node XMLNode that contains RTProperties node. * @return URN String of URN. * @throws ParsingException */ private static String parseUrn(XMLNode node) throws ParsingException { if (node.getChildren().size() != 1) throw new ParsingException("Expect RTPProperties node to only have one child"); XMLNode typeNode = node.getChildren().get(0); if (typeNode.getChildren().size() != 1) { throw new ParsingException("Expect Type node to only have one child"); } if (!TextUtils.equals(typeNode.getTag(), TAG_TYPE)) { throw new ParsingException("Unexpected tag for Type: " + typeNode.getTag()); } XMLNode ddfNameNode = typeNode.getChildren().get(0); if (!ddfNameNode.getChildren().isEmpty()) { throw new ParsingException("Expect DDFName node to have no child"); } if (!TextUtils.equals(ddfNameNode.getTag(), TAG_DDF_NAME)) { throw new ParsingException("Unexpected tag for DDFName: " + ddfNameNode.getTag()); } return ddfNameNode.getText(); } /** * Convert a XML tree represented by XMLNode to a PPS (PerProviderSubscription) instance tree * represented by PPSNode. This flattens out the XML tree to allow easier and cleaner parsing * of the PPS configuration data. Only three types of XML tag are expected: "NodeName", * "Node", and "Value". * * The original XML tree (each XML element represent a node): * * * root * * child1 * value1 * * * child2 * * grandchild1 * ... * * * ... * * * The converted PPS tree: * * [root] --- [child1, value1] * | * ---------[child2] --------[grandchild1] --- ... * * @param node XMLNode pointed to the root of a XML tree * @return PPSNode pointing to the root of a PPS tree * @throws ParsingException */ private static PPSNode buildPpsNode(XMLNode node) throws ParsingException { String nodeName = null; String nodeValue = null; List childNodes = new ArrayList(); // Names of parsed child nodes, use for detecting multiple child nodes with the same name. Set parsedNodes = new HashSet(); for (XMLNode child : node.getChildren()) { String tag = child.getTag(); if (TextUtils.equals(tag, TAG_NODE_NAME)) { if (nodeName != null) { throw new ParsingException("Duplicate NodeName node"); } nodeName = child.getText(); } else if (TextUtils.equals(tag, TAG_NODE)) { PPSNode ppsNode = buildPpsNode(child); if (parsedNodes.contains(ppsNode.getName())) { throw new ParsingException("Duplicate node: " + ppsNode.getName()); } parsedNodes.add(ppsNode.getName()); childNodes.add(ppsNode); } else if (TextUtils.equals(tag, TAG_VALUE)) { if (nodeValue != null) { throw new ParsingException("Duplicate Value node"); } nodeValue = child.getText(); } else { throw new ParsingException("Unknown tag: " + tag); } } if (nodeName == null) { throw new ParsingException("Invalid node: missing NodeName"); } if (nodeValue == null && childNodes.size() == 0) { throw new ParsingException("Invalid node: " + nodeName + " missing both value and children"); } if (nodeValue != null && childNodes.size() > 0) { throw new ParsingException("Invalid node: " + nodeName + " contained both value and children"); } if (nodeValue != null) { return new LeafNode(nodeName, nodeValue); } return new InternalNode(nodeName, childNodes); } /** * Return the value of a PPSNode. An exception will be thrown if the given node * is not a leaf node. * * @param node PPSNode to retrieve the value from * @return String representing the value of the node * @throws ParsingException */ private static String getPpsNodeValue(PPSNode node) throws ParsingException { if (!node.isLeaf()) { throw new ParsingException("Cannot get value from a non-leaf node: " + node.getName()); } return node.getValue(); } /** * Parse a PPS (PerProviderSubscription) configurations from a PPS tree. * * @param root PPSNode representing the root of the PPS tree * @return PasspointConfiguration * @throws ParsingException */ private static PasspointConfiguration parsePpsInstance(PPSNode root) throws ParsingException { if (root.isLeaf()) { throw new ParsingException("Leaf node not expected for PPS instance"); } PasspointConfiguration config = new PasspointConfiguration(); for (PPSNode child : root.getChildren()) { switch(child.getName()) { case NODE_HOMESP: config.setHomeSp(parseHomeSP(child)); break; case NODE_CREDENTIAL: config.setCredential(parseCredential(child)); break; case NODE_POLICY: config.setPolicy(parsePolicy(child)); break; case NODE_AAA_SERVER_TRUST_ROOT: config.setTrustRootCertList(parseAAAServerTrustRootList(child)); break; case NODE_SUBSCRIPTION_UPDATE: config.setSubscriptionUpdate(parseUpdateParameter(child)); break; case NODE_SUBSCRIPTION_PARAMETER: parseSubscriptionParameter(child, config); break; case NODE_CREDENTIAL_PRIORITY: config.setCredentialPriority(parseInteger(getPpsNodeValue(child))); break; case NODE_EXTENSION: // All vendor specific information will be under this node. Log.d(TAG, "Ignore Extension node for vendor specific information"); break; default: throw new ParsingException("Unknown node: " + child.getName()); } } return config; } /** * Parse configurations under PerProviderSubscription/HomeSP subtree. * * @param node PPSNode representing the root of the PerProviderSubscription/HomeSP subtree * @return HomeSP * @throws ParsingException */ private static HomeSp parseHomeSP(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for HomeSP"); } HomeSp homeSp = new HomeSp(); for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_FQDN: homeSp.setFqdn(getPpsNodeValue(child)); break; case NODE_FRIENDLY_NAME: homeSp.setFriendlyName(getPpsNodeValue(child)); break; case NODE_ROAMING_CONSORTIUM_OI: homeSp.setRoamingConsortiumOis( parseRoamingConsortiumOI(getPpsNodeValue(child))); break; case NODE_ICON_URL: homeSp.setIconUrl(getPpsNodeValue(child)); break; case NODE_NETWORK_ID: homeSp.setHomeNetworkIds(parseNetworkIds(child)); break; case NODE_HOME_OI_LIST: Pair, List> homeOIs = parseHomeOIList(child); homeSp.setMatchAllOis(convertFromLongList(homeOIs.first)); homeSp.setMatchAnyOis(convertFromLongList(homeOIs.second)); break; case NODE_OTHER_HOME_PARTNERS: homeSp.setOtherHomePartners(parseOtherHomePartners(child)); break; default: throw new ParsingException("Unknown node under HomeSP: " + child.getName()); } } return homeSp; } /** * Parse the roaming consortium OI string, which contains a list of OIs separated by ",". * * @param oiStr string containing list of OIs (Organization Identifiers) separated by "," * @return long[] * @throws ParsingException */ private static long[] parseRoamingConsortiumOI(String oiStr) throws ParsingException { String[] oiStrArray = oiStr.split(","); long[] oiArray = new long[oiStrArray.length]; for (int i = 0; i < oiStrArray.length; i++) { oiArray[i] = parseLong(oiStrArray[i], 16); } return oiArray; } /** * Parse configurations under PerProviderSubscription/HomeSP/NetworkID subtree. * * @param node PPSNode representing the root of the PerProviderSubscription/HomeSP/NetworkID * subtree * @return HashMap representing list of pair. * @throws ParsingException */ static private Map parseNetworkIds(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for NetworkID"); } Map networkIds = new HashMap<>(); for (PPSNode child : node.getChildren()) { Pair networkId = parseNetworkIdInstance(child); networkIds.put(networkId.first, networkId.second); } return networkIds; } /** * Parse configurations under PerProviderSubscription/HomeSP/NetworkID/ subtree. * The instance name () is irrelevant and must be unique for each instance, which * is verified when the PPS tree is constructed {@link #buildPpsNode}. * * @param node PPSNode representing the root of the * PerProviderSubscription/HomeSP/NetworkID/ subtree * @return Pair representing pair. * @throws ParsingException */ static private Pair parseNetworkIdInstance(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for NetworkID instance"); } String ssid = null; Long hessid = null; for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_SSID: ssid = getPpsNodeValue(child); break; case NODE_HESSID: hessid = parseLong(getPpsNodeValue(child), 16); break; default: throw new ParsingException("Unknown node under NetworkID instance: " + child.getName()); } } if (ssid == null) throw new ParsingException("NetworkID instance missing SSID"); return new Pair(ssid, hessid); } /** * Parse configurations under PerProviderSubscription/HomeSP/HomeOIList subtree. * * @param node PPSNode representing the root of the PerProviderSubscription/HomeSP/HomeOIList * subtree * @return Pair, List> containing both MatchAllOIs and MatchAnyOIs list. * @throws ParsingException */ private static Pair, List> parseHomeOIList(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for HomeOIList"); } List matchAllOIs = new ArrayList(); List matchAnyOIs = new ArrayList(); for (PPSNode child : node.getChildren()) { Pair homeOI = parseHomeOIInstance(child); if (homeOI.second.booleanValue()) { matchAllOIs.add(homeOI.first); } else { matchAnyOIs.add(homeOI.first); } } return new Pair, List>(matchAllOIs, matchAnyOIs); } /** * Parse configurations under PerProviderSubscription/HomeSP/HomeOIList/ subtree. * The instance name () is irrelevant and must be unique for each instance, which * is verified when the PPS tree is constructed {@link #buildPpsNode}. * * @param node PPSNode representing the root of the * PerProviderSubscription/HomeSP/HomeOIList/ subtree * @return Pair containing a HomeOI and a HomeOIRequired flag * @throws ParsingException */ private static Pair parseHomeOIInstance(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for HomeOI instance"); } Long oi = null; Boolean required = null; for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_HOME_OI: try { oi = Long.valueOf(getPpsNodeValue(child), 16); } catch (NumberFormatException e) { throw new ParsingException("Invalid HomeOI: " + getPpsNodeValue(child)); } break; case NODE_HOME_OI_REQUIRED: required = Boolean.valueOf(getPpsNodeValue(child)); break; default: throw new ParsingException("Unknown node under NetworkID instance: " + child.getName()); } } if (oi == null) { throw new ParsingException("HomeOI instance missing OI field"); } if (required == null) { throw new ParsingException("HomeOI instance missing required field"); } return new Pair(oi, required); } /** * Parse configurations under PerProviderSubscription/HomeSP/OtherHomePartners subtree. * This contains a list of FQDN (Fully Qualified Domain Name) that are considered * home partners. * * @param node PPSNode representing the root of the * PerProviderSubscription/HomeSP/OtherHomePartners subtree * @return String[] list of partner's FQDN * @throws ParsingException */ private static String[] parseOtherHomePartners(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for OtherHomePartners"); } List otherHomePartners = new ArrayList(); for (PPSNode child : node.getChildren()) { String fqdn = parseOtherHomePartnerInstance(child); otherHomePartners.add(fqdn); } return otherHomePartners.toArray(new String[otherHomePartners.size()]); } /** * Parse configurations under PerProviderSubscription/HomeSP/OtherHomePartners/ subtree. * The instance name () is irrelevant and must be unique for each instance, which * is verified when the PPS tree is constructed {@link #buildPpsNode}. * * @param node PPSNode representing the root of the * PerProviderSubscription/HomeSP/OtherHomePartners/ subtree * @return String FQDN of the partner * @throws ParsingException */ private static String parseOtherHomePartnerInstance(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for OtherHomePartner instance"); } String fqdn = null; for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_FQDN: fqdn = getPpsNodeValue(child); break; default: throw new ParsingException( "Unknown node under OtherHomePartner instance: " + child.getName()); } } if (fqdn == null) { throw new ParsingException("OtherHomePartner instance missing FQDN field"); } return fqdn; } /** * Parse configurations under PerProviderSubscription/Credential subtree. * * @param node PPSNode representing the root of the PerProviderSubscription/Credential subtree * @return Credential * @throws ParsingException */ private static Credential parseCredential(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for HomeSP"); } Credential credential = new Credential(); for (PPSNode child: node.getChildren()) { switch (child.getName()) { case NODE_CREATION_DATE: credential.setCreationTimeInMillis(parseDate(getPpsNodeValue(child))); break; case NODE_EXPIRATION_DATE: credential.setExpirationTimeInMillis(parseDate(getPpsNodeValue(child))); break; case NODE_USERNAME_PASSWORD: credential.setUserCredential(parseUserCredential(child)); break; case NODE_DIGITAL_CERTIFICATE: credential.setCertCredential(parseCertificateCredential(child)); break; case NODE_REALM: credential.setRealm(getPpsNodeValue(child)); break; case NODE_CHECK_AAA_SERVER_CERT_STATUS: credential.setCheckAaaServerCertStatus( Boolean.parseBoolean(getPpsNodeValue(child))); break; case NODE_SIM: credential.setSimCredential(parseSimCredential(child)); break; default: throw new ParsingException("Unknown node under Credential: " + child.getName()); } } return credential; } /** * Parse configurations under PerProviderSubscription/Credential/UsernamePassword subtree. * * @param node PPSNode representing the root of the * PerProviderSubscription/Credential/UsernamePassword subtree * @return Credential.UserCredential * @throws ParsingException */ private static Credential.UserCredential parseUserCredential(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for UsernamePassword"); } Credential.UserCredential userCred = new Credential.UserCredential(); for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_USERNAME: userCred.setUsername(getPpsNodeValue(child)); break; case NODE_PASSWORD: userCred.setPassword(getPpsNodeValue(child)); break; case NODE_MACHINE_MANAGED: userCred.setMachineManaged(Boolean.parseBoolean(getPpsNodeValue(child))); break; case NODE_SOFT_TOKEN_APP: userCred.setSoftTokenApp(getPpsNodeValue(child)); break; case NODE_ABLE_TO_SHARE: userCred.setAbleToShare(Boolean.parseBoolean(getPpsNodeValue(child))); break; case NODE_EAP_METHOD: parseEAPMethod(child, userCred); break; default: throw new ParsingException("Unknown node under UsernamPassword: " + child.getName()); } } return userCred; } /** * Parse configurations under PerProviderSubscription/Credential/UsernamePassword/EAPMethod * subtree. * * @param node PPSNode representing the root of the * PerProviderSubscription/Credential/UsernamePassword/EAPMethod subtree * @param userCred UserCredential to be updated with EAP method values. * @throws ParsingException */ private static void parseEAPMethod(PPSNode node, Credential.UserCredential userCred) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for EAPMethod"); } for (PPSNode child : node.getChildren()) { switch(child.getName()) { case NODE_EAP_TYPE: userCred.setEapType(parseInteger(getPpsNodeValue(child))); break; case NODE_INNER_METHOD: userCred.setNonEapInnerMethod(getPpsNodeValue(child)); break; case NODE_VENDOR_ID: case NODE_VENDOR_TYPE: case NODE_INNER_EAP_TYPE: case NODE_INNER_VENDOR_ID: case NODE_INNER_VENDOR_TYPE: // Only EAP-TTLS is currently supported for user credential, which doesn't // use any of these parameters. Log.d(TAG, "Ignore unsupported EAP method parameter: " + child.getName()); break; default: throw new ParsingException("Unknown node under EAPMethod: " + child.getName()); } } } /** * Parse configurations under PerProviderSubscription/Credential/DigitalCertificate subtree. * * @param node PPSNode representing the root of the * PerProviderSubscription/Credential/DigitalCertificate subtree * @return Credential.CertificateCredential * @throws ParsingException */ private static Credential.CertificateCredential parseCertificateCredential(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for DigitalCertificate"); } Credential.CertificateCredential certCred = new Credential.CertificateCredential(); for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_CERTIFICATE_TYPE: certCred.setCertType(getPpsNodeValue(child)); break; case NODE_CERT_SHA256_FINGERPRINT: certCred.setCertSha256Fingerprint(parseHexString(getPpsNodeValue(child))); break; default: throw new ParsingException("Unknown node under DigitalCertificate: " + child.getName()); } } return certCred; } /** * Parse configurations under PerProviderSubscription/Credential/SIM subtree. * * @param node PPSNode representing the root of the PerProviderSubscription/Credential/SIM * subtree * @return Credential.SimCredential * @throws ParsingException */ private static Credential.SimCredential parseSimCredential(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for SIM"); } Credential.SimCredential simCred = new Credential.SimCredential(); for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_SIM_IMSI: simCred.setImsi(getPpsNodeValue(child)); break; case NODE_EAP_TYPE: simCred.setEapType(parseInteger(getPpsNodeValue(child))); break; default: throw new ParsingException("Unknown node under SIM: " + child.getName()); } } return simCred; } /** * Parse configurations under PerProviderSubscription/Policy subtree. * * @param node PPSNode representing the root of the PerProviderSubscription/Policy subtree * @return {@link Policy} * @throws ParsingException */ private static Policy parsePolicy(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for Policy"); } Policy policy = new Policy(); for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_PREFERRED_ROAMING_PARTNER_LIST: policy.setPreferredRoamingPartnerList(parsePreferredRoamingPartnerList(child)); break; case NODE_MIN_BACKHAUL_THRESHOLD: parseMinBackhaulThreshold(child, policy); break; case NODE_POLICY_UPDATE: policy.setPolicyUpdate(parseUpdateParameter(child)); break; case NODE_SP_EXCLUSION_LIST: policy.setExcludedSsidList(parseSpExclusionList(child)); break; case NODE_REQUIRED_PROTO_PORT_TUPLE: policy.setRequiredProtoPortMap(parseRequiredProtoPortTuple(child)); break; case NODE_MAXIMUM_BSS_LOAD_VALUE: policy.setMaximumBssLoadValue(parseInteger(getPpsNodeValue(child))); break; default: throw new ParsingException("Unknown node under Policy: " + child.getName()); } } return policy; } /** * Parse configurations under PerProviderSubscription/Policy/PreferredRoamingPartnerList * subtree. * * @param node PPSNode representing the root of the * PerProviderSubscription/Policy/PreferredRoamingPartnerList subtree * @return List of {@link Policy#RoamingPartner} * @throws ParsingException */ private static List parsePreferredRoamingPartnerList(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for PreferredRoamingPartnerList"); } List partnerList = new ArrayList<>(); for (PPSNode child : node.getChildren()) { partnerList.add(parsePreferredRoamingPartner(child)); } return partnerList; } /** * Parse configurations under PerProviderSubscription/Policy/PreferredRoamingPartnerList/ * subtree. * * @param node PPSNode representing the root of the * PerProviderSubscription/Policy/PreferredRoamingPartnerList/ subtree * @return {@link Policy#RoamingPartner} * @throws ParsingException */ private static Policy.RoamingPartner parsePreferredRoamingPartner(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for PreferredRoamingPartner " + "instance"); } Policy.RoamingPartner roamingPartner = new Policy.RoamingPartner(); for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_FQDN_MATCH: // FQDN_Match field is in the format of "[FQDN],[MatchInfo]", where [MatchInfo] // is either "exactMatch" for exact match of FQDN or "includeSubdomains" for // matching all FQDNs with the same sub-domain. String fqdnMatch = getPpsNodeValue(child); String[] fqdnMatchArray = fqdnMatch.split(","); if (fqdnMatchArray.length != 2) { throw new ParsingException("Invalid FQDN_Match: " + fqdnMatch); } roamingPartner.setFqdn(fqdnMatchArray[0]); if (TextUtils.equals(fqdnMatchArray[1], "exactMatch")) { roamingPartner.setFqdnExactMatch(true); } else if (TextUtils.equals(fqdnMatchArray[1], "includeSubdomains")) { roamingPartner.setFqdnExactMatch(false); } else { throw new ParsingException("Invalid FQDN_Match: " + fqdnMatch); } break; case NODE_PRIORITY: roamingPartner.setPriority(parseInteger(getPpsNodeValue(child))); break; case NODE_COUNTRY: roamingPartner.setCountries(getPpsNodeValue(child)); break; default: throw new ParsingException("Unknown node under PreferredRoamingPartnerList " + "instance " + child.getName()); } } return roamingPartner; } /** * Parse configurations under PerProviderSubscription/Policy/MinBackhaulThreshold subtree * into the given policy. * * @param node PPSNode representing the root of the * PerProviderSubscription/Policy/MinBackhaulThreshold subtree * @param policy The policy to store the MinBackhualThreshold configuration * @throws ParsingException */ private static void parseMinBackhaulThreshold(PPSNode node, Policy policy) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for MinBackhaulThreshold"); } for (PPSNode child : node.getChildren()) { parseMinBackhaulThresholdInstance(child, policy); } } /** * Parse configurations under PerProviderSubscription/Policy/MinBackhaulThreshold/ subtree * into the given policy. * * @param node PPSNode representing the root of the * PerProviderSubscription/Policy/MinBackhaulThreshold/ subtree * @param policy The policy to store the MinBackhaulThreshold configuration * @throws ParsingException */ private static void parseMinBackhaulThresholdInstance(PPSNode node, Policy policy) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for MinBackhaulThreshold instance"); } String networkType = null; long downlinkBandwidth = Long.MIN_VALUE; long uplinkBandwidth = Long.MIN_VALUE; for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_NETWORK_TYPE: networkType = getPpsNodeValue(child); break; case NODE_DOWNLINK_BANDWIDTH: downlinkBandwidth = parseLong(getPpsNodeValue(child), 10); break; case NODE_UPLINK_BANDWIDTH: uplinkBandwidth = parseLong(getPpsNodeValue(child), 10); break; default: throw new ParsingException("Unknown node under MinBackhaulThreshold instance " + child.getName()); } } if (networkType == null) { throw new ParsingException("Missing NetworkType field"); } if (TextUtils.equals(networkType, "home")) { policy.setMinHomeDownlinkBandwidth(downlinkBandwidth); policy.setMinHomeUplinkBandwidth(uplinkBandwidth); } else if (TextUtils.equals(networkType, "roaming")) { policy.setMinRoamingDownlinkBandwidth(downlinkBandwidth); policy.setMinRoamingUplinkBandwidth(uplinkBandwidth); } else { throw new ParsingException("Invalid network type: " + networkType); } } /** * Parse update parameters. This contained configurations from either * PerProviderSubscription/Policy/PolicyUpdate or PerProviderSubscription/SubscriptionUpdate * subtree. * * @param node PPSNode representing the root of the PerProviderSubscription/Policy/PolicyUpdate * or PerProviderSubscription/SubscriptionUpdate subtree * @return {@link UpdateParameter} * @throws ParsingException */ private static UpdateParameter parseUpdateParameter(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for Update Parameters"); } UpdateParameter updateParam = new UpdateParameter(); for (PPSNode child : node.getChildren()) { switch(child.getName()) { case NODE_UPDATE_INTERVAL: updateParam.setUpdateIntervalInMinutes(parseLong(getPpsNodeValue(child), 10)); break; case NODE_UPDATE_METHOD: updateParam.setUpdateMethod(getPpsNodeValue(child)); break; case NODE_RESTRICTION: updateParam.setRestriction(getPpsNodeValue(child)); break; case NODE_URI: updateParam.setServerUri(getPpsNodeValue(child)); break; case NODE_USERNAME_PASSWORD: Pair usernamePassword = parseUpdateUserCredential(child); updateParam.setUsername(usernamePassword.first); updateParam.setBase64EncodedPassword(usernamePassword.second); break; case NODE_TRUST_ROOT: Pair trustRoot = parseTrustRoot(child); updateParam.setTrustRootCertUrl(trustRoot.first); updateParam.setTrustRootCertSha256Fingerprint(trustRoot.second); break; case NODE_OTHER: Log.d(TAG, "Ignore unsupported paramter: " + child.getName()); break; default: throw new ParsingException("Unknown node under Update Parameters: " + child.getName()); } } return updateParam; } /** * Parse username and password parameters associated with policy or subscription update. * This contained configurations under either * PerProviderSubscription/Policy/PolicyUpdate/UsernamePassword or * PerProviderSubscription/SubscriptionUpdate/UsernamePassword subtree. * * @param node PPSNode representing the root of the UsernamePassword subtree * @return Pair of username and password * @throws ParsingException */ private static Pair parseUpdateUserCredential(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for UsernamePassword"); } String username = null; String password = null; for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_USERNAME: username = getPpsNodeValue(child); break; case NODE_PASSWORD: password = getPpsNodeValue(child); break; default: throw new ParsingException("Unknown node under UsernamePassword: " + child.getName()); } } return Pair.create(username, password); } /** * Parse the trust root parameters associated with policy update, subscription update, or AAA * server trust root. * * This contained configurations under either * PerProviderSubscription/Policy/PolicyUpdate/TrustRoot or * PerProviderSubscription/SubscriptionUpdate/TrustRoot or * PerProviderSubscription/AAAServerTrustRoot/ subtree. * * @param node PPSNode representing the root of the TrustRoot subtree * @return Pair of Certificate URL and fingerprint * @throws ParsingException */ private static Pair parseTrustRoot(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for TrustRoot"); } String certUrl = null; byte[] certFingerprint = null; for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_CERT_URL: certUrl = getPpsNodeValue(child); break; case NODE_CERT_SHA256_FINGERPRINT: certFingerprint = parseHexString(getPpsNodeValue(child)); break; default: throw new ParsingException("Unknown node under TrustRoot: " + child.getName()); } } return Pair.create(certUrl, certFingerprint); } /** * Parse configurations under PerProviderSubscription/Policy/SPExclusionList subtree. * * @param node PPSNode representing the root of the * PerProviderSubscription/Policy/SPExclusionList subtree * @return Array of excluded SSIDs * @throws ParsingException */ private static String[] parseSpExclusionList(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for SPExclusionList"); } List ssidList = new ArrayList<>(); for (PPSNode child : node.getChildren()) { ssidList.add(parseSpExclusionInstance(child)); } return ssidList.toArray(new String[ssidList.size()]); } /** * Parse configurations under PerProviderSubscription/Policy/SPExclusionList/ subtree. * * @param node PPSNode representing the root of the * PerProviderSubscription/Policy/SPExclusionList/ subtree * @return String * @throws ParsingException */ private static String parseSpExclusionInstance(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for SPExclusion instance"); } String ssid = null; for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_SSID: ssid = getPpsNodeValue(child); break; default: throw new ParsingException("Unknown node under SPExclusion instance"); } } return ssid; } /** * Parse configurations under PerProviderSubscription/Policy/RequiredProtoPortTuple subtree. * * @param node PPSNode representing the root of the * PerProviderSubscription/Policy/RequiredProtoPortTuple subtree * @return Map of IP Protocol to Port Number tuples * @throws ParsingException */ private static Map parseRequiredProtoPortTuple(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for RequiredProtoPortTuple"); } Map protoPortTupleMap = new HashMap<>(); for (PPSNode child : node.getChildren()) { Pair protoPortTuple = parseProtoPortTuple(child); protoPortTupleMap.put(protoPortTuple.first, protoPortTuple.second); } return protoPortTupleMap; } /** * Parse configurations under PerProviderSubscription/Policy/RequiredProtoPortTuple/ * subtree. * * @param node PPSNode representing the root of the * PerProviderSubscription/Policy/RequiredProtoPortTuple/ subtree * @return Pair of IP Protocol to Port Number tuple * @throws ParsingException */ private static Pair parseProtoPortTuple(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for RequiredProtoPortTuple " + "instance"); } int proto = Integer.MIN_VALUE; String ports = null; for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_IP_PROTOCOL: proto = parseInteger(getPpsNodeValue(child)); break; case NODE_PORT_NUMBER: ports = getPpsNodeValue(child); break; default: throw new ParsingException("Unknown node under RequiredProtoPortTuple instance" + child.getName()); } } if (proto == Integer.MIN_VALUE) { throw new ParsingException("Missing IPProtocol field"); } if (ports == null) { throw new ParsingException("Missing PortNumber field"); } return Pair.create(proto, ports); } /** * Parse configurations under PerProviderSubscription/AAAServerTrustRoot subtree. * * @param node PPSNode representing the root of PerProviderSubscription/AAAServerTrustRoot * subtree * @return Map of certificate URL with the corresponding certificate fingerprint * @throws ParsingException */ private static Map parseAAAServerTrustRootList(PPSNode node) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for AAAServerTrustRoot"); } Map certList = new HashMap<>(); for (PPSNode child : node.getChildren()) { Pair certTuple = parseTrustRoot(child); certList.put(certTuple.first, certTuple.second); } return certList; } /** * Parse configurations under PerProviderSubscription/SubscriptionParameter subtree. * * @param node PPSNode representing the root of PerProviderSubscription/SubscriptionParameter * subtree * @param config Instance of {@link PasspointConfiguration} * @throws ParsingException */ private static void parseSubscriptionParameter(PPSNode node, PasspointConfiguration config) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for SubscriptionParameter"); } for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_CREATION_DATE: config.setSubscriptionCreationTimeInMillis(parseDate(getPpsNodeValue(child))); break; case NODE_EXPIRATION_DATE: config.setSubscriptionExpirationTimeInMillis(parseDate(getPpsNodeValue(child))); break; case NODE_TYPE_OF_SUBSCRIPTION: config.setSubscriptionType(getPpsNodeValue(child)); break; case NODE_USAGE_LIMITS: parseUsageLimits(child, config); break; default: throw new ParsingException("Unknown node under SubscriptionParameter" + child.getName()); } } } /** * Parse configurations under PerProviderSubscription/SubscriptionParameter/UsageLimits * subtree. * * @param node PPSNode representing the root of * PerProviderSubscription/SubscriptionParameter/UsageLimits subtree * @param config Instance of {@link PasspointConfiguration} * @throws ParsingException */ private static void parseUsageLimits(PPSNode node, PasspointConfiguration config) throws ParsingException { if (node.isLeaf()) { throw new ParsingException("Leaf node not expected for UsageLimits"); } for (PPSNode child : node.getChildren()) { switch (child.getName()) { case NODE_DATA_LIMIT: config.setUsageLimitDataLimit(parseLong(getPpsNodeValue(child), 10)); break; case NODE_START_DATE: config.setUsageLimitStartTimeInMillis(parseDate(getPpsNodeValue(child))); break; case NODE_TIME_LIMIT: config.setUsageLimitTimeLimitInMinutes(parseLong(getPpsNodeValue(child), 10)); break; case NODE_USAGE_TIME_PERIOD: config.setUsageLimitUsageTimePeriodInMinutes( parseLong(getPpsNodeValue(child), 10)); break; default: throw new ParsingException("Unknown node under UsageLimits" + child.getName()); } } } /** * Convert a hex string to a byte array. * * @param str String containing hex values * @return byte[] * @throws ParsingException */ private static byte[] parseHexString(String str) throws ParsingException { if ((str.length() & 1) == 1) { throw new ParsingException("Odd length hex string: " + str.length()); } byte[] result = new byte[str.length() / 2]; for (int i = 0; i < result.length; i++) { int index = i * 2; try { result[i] = (byte) Integer.parseInt(str.substring(index, index + 2), 16); } catch (NumberFormatException e) { throw new ParsingException("Invalid hex string: " + str); } } return result; } /** * Convert a date string to the number of milliseconds since January 1, 1970, 00:00:00 GMT. * * @param dateStr String in the format of yyyy-MM-dd'T'HH:mm:ss'Z' * @return number of milliseconds * @throws ParsingException */ private static long parseDate(String dateStr) throws ParsingException { try { DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); return format.parse(dateStr).getTime(); } catch (ParseException pe) { throw new ParsingException("Badly formatted time: " + dateStr); } } /** * Parse an integer string. * * @param value String of integer value * @return int * @throws ParsingException */ private static int parseInteger(String value) throws ParsingException { try { return Integer.parseInt(value); } catch (NumberFormatException e) { throw new ParsingException("Invalid integer value: " + value); } } /** * Parse a string representing a long integer. * * @param value String of long integer value * @return long * @throws ParsingException */ private static long parseLong(String value, int radix) throws ParsingException { try { return Long.parseLong(value, radix); } catch (NumberFormatException e) { throw new ParsingException("Invalid long integer value: " + value); } } /** * Convert a List to a primitive long array long[]. * * @param list List to be converted * @return long[] */ private static long[] convertFromLongList(List list) { Long[] objectArray = list.toArray(new Long[list.size()]); long[] primitiveArray = new long[objectArray.length]; for (int i = 0; i < objectArray.length; i++) { primitiveArray[i] = objectArray[i].longValue(); } return primitiveArray; } }