1 /**
2  * Copyright (c) 2016, The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.net.wifi.hotspot2.omadm;
18 
19 import android.net.wifi.hotspot2.PasspointConfiguration;
20 import android.net.wifi.hotspot2.pps.Credential;
21 import android.net.wifi.hotspot2.pps.HomeSp;
22 import android.net.wifi.hotspot2.pps.Policy;
23 import android.net.wifi.hotspot2.pps.UpdateParameter;
24 import android.text.TextUtils;
25 import android.util.Log;
26 import android.util.Pair;
27 
28 import org.xml.sax.SAXException;
29 
30 import java.io.IOException;
31 import java.text.DateFormat;
32 import java.text.ParseException;
33 import java.text.SimpleDateFormat;
34 import java.util.ArrayList;
35 import java.util.HashMap;
36 import java.util.HashSet;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.Set;
40 
41 /**
42  * Utility class for converting OMA-DM (Open Mobile Alliance's Device Management)
43  * PPS-MO (PerProviderSubscription Management Object) XML tree to a
44  * {@link PasspointConfiguration} object.
45  *
46  * Currently this only supports PerProviderSubscription/HomeSP and
47  * PerProviderSubscription/Credential subtree for Hotspot 2.0 Release 1 support.
48  *
49  * For more info, refer to Hotspot 2.0 PPS MO defined in section 9.1 of the Hotspot 2.0
50  * Release 2 Technical Specification.
51  *
52  * Below is a sample XML string for a Release 1 PPS MO tree:
53  *
54  * <MgmtTree xmlns="syncml:dmddf1.2">
55  *   <VerDTD>1.2</VerDTD>
56  *   <Node>
57  *     <NodeName>PerProviderSubscription</NodeName>
58  *     <RTProperties>
59  *       <Type>
60  *         <DDFName>urn:wfa:mo:hotspot2dot0­perprovidersubscription:1.0</DDFName>
61  *       </Type>
62  *     </RTProperties>
63  *     <Node>
64  *       <NodeName>i001</NodeName>
65  *       <Node>
66  *         <NodeName>HomeSP</NodeName>
67  *         <Node>
68  *           <NodeName>FriendlyName</NodeName>
69  *           <Value>Century House</Value>
70  *         </Node>
71  *         <Node>
72  *           <NodeName>FQDN</NodeName>
73  *           <Value>mi6.co.uk</Value>
74  *         </Node>
75  *         <Node>
76  *           <NodeName>RoamingConsortiumOI</NodeName>
77  *           <Value>112233,445566</Value>
78  *         </Node>
79  *       </Node>
80  *       <Node>
81  *         <NodeName>Credential</NodeName>
82  *         <Node>
83  *           <NodeName>Realm</NodeName>
84  *           <Value>shaken.stirred.com</Value>
85  *         </Node>
86  *         <Node>
87  *           <NodeName>UsernamePassword</NodeName>
88  *           <Node>
89  *             <NodeName>Username</NodeName>
90  *             <Value>james</Value>
91  *           </Node>
92  *           <Node>
93  *             <NodeName>Password</NodeName>
94  *             <Value>Ym9uZDAwNw==</Value>
95  *           </Node>
96  *           <Node>
97  *             <NodeName>EAPMethod</NodeName>
98  *             <Node>
99  *               <NodeName>EAPType</NodeName>
100  *               <Value>21</Value>
101  *             </Node>
102  *             <Node>
103  *               <NodeName>InnerMethod</NodeName>
104  *               <Value>MS-CHAP-V2</Value>
105  *             </Node>
106  *           </Node>
107  *         </Node>
108  *       </Node>
109  *     </Node>
110  *   </Node>
111  * </MgmtTree>
112  */
113 public final class PpsMoParser {
114     private static final String TAG = "PpsMoParser";
115 
116     /**
117      * XML tags expected in the PPS MO (PerProviderSubscription Management Object) XML tree.
118      */
119     private static final String TAG_MANAGEMENT_TREE = "MgmtTree";
120     private static final String TAG_VER_DTD = "VerDTD";
121     private static final String TAG_NODE = "Node";
122     private static final String TAG_NODE_NAME = "NodeName";
123     private static final String TAG_RT_PROPERTIES = "RTProperties";
124     private static final String TAG_TYPE = "Type";
125     private static final String TAG_DDF_NAME = "DDFName";
126     private static final String TAG_VALUE = "Value";
127 
128     /**
129      * Name for PerProviderSubscription node.
130      */
131     private static final String NODE_PER_PROVIDER_SUBSCRIPTION = "PerProviderSubscription";
132 
133     /**
134      * Fields under PerProviderSubscription.
135      */
136     private static final String NODE_UPDATE_IDENTIFIER = "UpdateIdentifier";
137     private static final String NODE_AAA_SERVER_TRUST_ROOT = "AAAServerTrustRoot";
138     private static final String NODE_SUBSCRIPTION_UPDATE = "SubscriptionUpdate";
139     private static final String NODE_SUBSCRIPTION_PARAMETER = "SubscriptionParameters";
140     private static final String NODE_TYPE_OF_SUBSCRIPTION = "TypeOfSubscription";
141     private static final String NODE_USAGE_LIMITS = "UsageLimits";
142     private static final String NODE_DATA_LIMIT = "DataLimit";
143     private static final String NODE_START_DATE = "StartDate";
144     private static final String NODE_TIME_LIMIT = "TimeLimit";
145     private static final String NODE_USAGE_TIME_PERIOD = "UsageTimePeriod";
146     private static final String NODE_CREDENTIAL_PRIORITY = "CredentialPriority";
147     private static final String NODE_EXTENSION = "Extension";
148 
149     /**
150      * Fields under Extension/Android subtree.
151      */
152     /*
153      * This node is used to put Android specific extension nodes and must be put
154      * under "Extension" node. Nodes with unknown names are allowed under this subtree.
155      * If there is any new node added in later release, it won't break older release parsing.
156      * <p>
157      * Ex:
158      * <Node>
159      *   <NodeName>Extension</NodeName>
160      *   <Node>
161      *     <NodeName>Android</NodeName>
162      *     <Node>
163      *       <NodeName>AndroidSpecificAttribute</NodeName>
164      *       <Value>AndroidSpecificValue</Value>
165      *     </Node>
166      *     <Node>
167      *       <NodeName>AndroidSpecificAttribute2</NodeName>
168      *       <Value>AndroidSpecificValue2</Value>
169      *     </Node>
170      *   </Node>
171      * </Node>
172      */
173     private static final String NODE_VENDOR_ANDROID = "Android";
174     /*
175      * This node describes AAA server trusted names. The trusted name must be put in
176      * a leaf named "FQDN". More than one trusted names can be provided by using
177      * semicolons to separate the strings (e.g., example.org;example.com).
178      * <p>
179      * Ex:
180      * <Node>
181      *   <NodeName>AAAServerTrustedNames</NodeName>
182      *   <Node>
183      *     <NodeName>FQDN</NodeName>
184      *     <Value>trusted.com;auth.net</Value>
185      *  </Node>
186      * <Node>
187      */
188     private static final String NODE_AAA_SERVER_TRUSTED_NAMES = "AAAServerTrustedNames";
189 
190     /**
191      * Fields under HomeSP subtree.
192      */
193     private static final String NODE_HOMESP = "HomeSP";
194     private static final String NODE_FQDN = "FQDN";
195     private static final String NODE_FRIENDLY_NAME = "FriendlyName";
196     private static final String NODE_ROAMING_CONSORTIUM_OI = "RoamingConsortiumOI";
197     private static final String NODE_NETWORK_ID = "NetworkID";
198     private static final String NODE_SSID = "SSID";
199     private static final String NODE_HESSID = "HESSID";
200     private static final String NODE_ICON_URL = "IconURL";
201     private static final String NODE_HOME_OI_LIST = "HomeOIList";
202     private static final String NODE_HOME_OI = "HomeOI";
203     private static final String NODE_HOME_OI_REQUIRED = "HomeOIRequired";
204     private static final String NODE_OTHER_HOME_PARTNERS = "OtherHomePartners";
205 
206     /**
207      * Fields under Credential subtree.
208      */
209     private static final String NODE_CREDENTIAL = "Credential";
210     private static final String NODE_CREATION_DATE = "CreationDate";
211     private static final String NODE_EXPIRATION_DATE = "ExpirationDate";
212     private static final String NODE_USERNAME_PASSWORD = "UsernamePassword";
213     private static final String NODE_USERNAME = "Username";
214     private static final String NODE_PASSWORD = "Password";
215     private static final String NODE_MACHINE_MANAGED = "MachineManaged";
216     private static final String NODE_SOFT_TOKEN_APP = "SoftTokenApp";
217     private static final String NODE_ABLE_TO_SHARE = "AbleToShare";
218     private static final String NODE_EAP_METHOD = "EAPMethod";
219     private static final String NODE_EAP_TYPE = "EAPType";
220     private static final String NODE_VENDOR_ID = "VendorId";
221     private static final String NODE_VENDOR_TYPE = "VendorType";
222     private static final String NODE_INNER_EAP_TYPE = "InnerEAPType";
223     private static final String NODE_INNER_VENDOR_ID = "InnerVendorID";
224     private static final String NODE_INNER_VENDOR_TYPE = "InnerVendorType";
225     private static final String NODE_INNER_METHOD = "InnerMethod";
226     private static final String NODE_DIGITAL_CERTIFICATE = "DigitalCertificate";
227     private static final String NODE_CERTIFICATE_TYPE = "CertificateType";
228     private static final String NODE_CERT_SHA256_FINGERPRINT = "CertSHA256Fingerprint";
229     private static final String NODE_REALM = "Realm";
230     private static final String NODE_SIM = "SIM";
231     private static final String NODE_SIM_IMSI = "IMSI";
232     private static final String NODE_CHECK_AAA_SERVER_CERT_STATUS = "CheckAAAServerCertStatus";
233 
234     /**
235      * Fields under Policy subtree.
236      */
237     private static final String NODE_POLICY = "Policy";
238     private static final String NODE_PREFERRED_ROAMING_PARTNER_LIST =
239             "PreferredRoamingPartnerList";
240     private static final String NODE_FQDN_MATCH = "FQDN_Match";
241     private static final String NODE_PRIORITY = "Priority";
242     private static final String NODE_COUNTRY = "Country";
243     private static final String NODE_MIN_BACKHAUL_THRESHOLD = "MinBackhaulThreshold";
244     private static final String NODE_NETWORK_TYPE = "NetworkType";
245     private static final String NODE_DOWNLINK_BANDWIDTH = "DLBandwidth";
246     private static final String NODE_UPLINK_BANDWIDTH = "ULBandwidth";
247     private static final String NODE_POLICY_UPDATE = "PolicyUpdate";
248     private static final String NODE_UPDATE_INTERVAL = "UpdateInterval";
249     private static final String NODE_UPDATE_METHOD = "UpdateMethod";
250     private static final String NODE_RESTRICTION = "Restriction";
251     private static final String NODE_URI = "URI";
252     private static final String NODE_TRUST_ROOT = "TrustRoot";
253     private static final String NODE_CERT_URL = "CertURL";
254     private static final String NODE_SP_EXCLUSION_LIST = "SPExclusionList";
255     private static final String NODE_REQUIRED_PROTO_PORT_TUPLE = "RequiredProtoPortTuple";
256     private static final String NODE_IP_PROTOCOL = "IPProtocol";
257     private static final String NODE_PORT_NUMBER = "PortNumber";
258     private static final String NODE_MAXIMUM_BSS_LOAD_VALUE = "MaximumBSSLoadValue";
259     private static final String NODE_OTHER = "Other";
260 
261     /**
262      * URN (Unique Resource Name) for PerProviderSubscription Management Object Tree.
263      */
264     private static final String PPS_MO_URN =
265             "urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0";
266 
267     /**
268      * Exception for generic parsing errors.
269      */
270     private static class ParsingException extends Exception {
ParsingException(String message)271         public ParsingException(String message) {
272             super(message);
273         }
274     }
275 
276     /**
277      * Class representing a node within the PerProviderSubscription tree.
278      * This is used to flatten out and eliminate the extra layering in the XMLNode tree,
279      * to make the data parsing easier and cleaner.
280      *
281      * A PPSNode can be an internal or a leaf node, but not both.
282      *
283      */
284     private static abstract class PPSNode {
285         private final String mName;
PPSNode(String name)286         public PPSNode(String name) {
287             mName = name;
288         }
289 
290         /**
291          * @return the name of the node
292          */
getName()293         public String getName() {
294             return mName;
295         }
296 
297         /**
298          * Applies for internal node only.
299          *
300          * @return the list of children nodes.
301          */
getChildren()302         public abstract List<PPSNode> getChildren();
303 
304         /**
305          * Applies for leaf node only.
306          *
307          * @return the string value of the node
308          */
getValue()309         public abstract String getValue();
310 
311         /**
312          * @return a flag indicating if this is a leaf or an internal node
313          */
isLeaf()314         public abstract boolean isLeaf();
315     }
316 
317     /**
318      * Class representing a leaf node in a PPS (PerProviderSubscription) tree.
319      */
320     private static class LeafNode extends PPSNode {
321         private final String mValue;
LeafNode(String nodeName, String value)322         public LeafNode(String nodeName, String value) {
323             super(nodeName);
324             mValue = value;
325         }
326 
327         @Override
getValue()328         public String getValue() {
329             return mValue;
330         }
331 
332         @Override
getChildren()333         public List<PPSNode> getChildren() {
334             return null;
335         }
336 
337         @Override
isLeaf()338         public boolean isLeaf() {
339             return true;
340         }
341     }
342 
343     /**
344      * Class representing an internal node in a PPS (PerProviderSubscription) tree.
345      */
346     private static class InternalNode extends PPSNode {
347         private final List<PPSNode> mChildren;
InternalNode(String nodeName, List<PPSNode> children)348         public InternalNode(String nodeName, List<PPSNode> children) {
349             super(nodeName);
350             mChildren = children;
351         }
352 
353         @Override
getValue()354         public String getValue() {
355             return null;
356         }
357 
358         @Override
getChildren()359         public List<PPSNode> getChildren() {
360             return mChildren;
361         }
362 
363         @Override
isLeaf()364         public boolean isLeaf() {
365             return false;
366         }
367     }
368 
369     /**
370      * @hide
371      */
PpsMoParser()372     public PpsMoParser() {}
373 
374     /**
375      * Convert a XML string representation of a PPS MO (PerProviderSubscription
376      * Management Object) tree to a {@link PasspointConfiguration} object.
377      *
378      * @param xmlString XML string representation of a PPS MO tree
379      * @return {@link PasspointConfiguration} or null
380      */
parseMoText(String xmlString)381     public static PasspointConfiguration parseMoText(String xmlString) {
382         // Convert the XML string to a XML tree.
383         XMLParser xmlParser = new XMLParser();
384         XMLNode root = null;
385         try {
386             root = xmlParser.parse(xmlString);
387         } catch(IOException | SAXException e) {
388             return null;
389         }
390         if (root == null) {
391             return null;
392         }
393 
394         // Verify root node is a "MgmtTree" node.
395         if (root.getTag() != TAG_MANAGEMENT_TREE) {
396             Log.e(TAG, "Root is not a MgmtTree");
397             return null;
398         }
399 
400         String verDtd = null;    // Used for detecting duplicate VerDTD element.
401         PasspointConfiguration config = null;
402         for (XMLNode child : root.getChildren()) {
403             switch(child.getTag()) {
404                 case TAG_VER_DTD:
405                     if (verDtd != null) {
406                         Log.e(TAG, "Duplicate VerDTD element");
407                         return null;
408                     }
409                     verDtd = child.getText();
410                     break;
411                 case TAG_NODE:
412                     if (config != null) {
413                         Log.e(TAG, "Unexpected multiple Node element under MgmtTree");
414                         return null;
415                     }
416                     try {
417                         config = parsePpsNode(child);
418                     } catch (ParsingException e) {
419                         Log.e(TAG, e.getMessage());
420                         return null;
421                     }
422                     break;
423                 default:
424                     Log.e(TAG, "Unknown node: " + child.getTag());
425                     return null;
426             }
427         }
428         return config;
429     }
430 
431     /**
432      * Parse a PerProviderSubscription node. Below is the format of the XML tree (with
433      * each XML element represent a node in the tree):
434      *
435      * <Node>
436      *   <NodeName>PerProviderSubscription</NodeName>
437      *   <RTProperties>
438      *     ...
439      *   </RTPProperties>
440      *   <Node>
441      *     <NodeName>UpdateIdentifier</NodeName>
442      *     <Value>...</Value>
443      *   </Node>
444      *   <Node>
445      *     ...
446      *   </Node>
447      * </Node>
448      *
449      * @param node XMLNode that contains PerProviderSubscription node.
450      * @return PasspointConfiguration or null
451      * @throws ParsingException
452      */
parsePpsNode(XMLNode node)453     private static PasspointConfiguration parsePpsNode(XMLNode node)
454             throws ParsingException {
455         PasspointConfiguration config = null;
456         String nodeName = null;
457         int updateIdentifier = Integer.MIN_VALUE;
458         for (XMLNode child : node.getChildren()) {
459             switch (child.getTag()) {
460                 case TAG_NODE_NAME:
461                     if (nodeName != null) {
462                         throw new ParsingException("Duplicate NodeName: " + child.getText());
463                     }
464                     nodeName = child.getText();
465                     if (!TextUtils.equals(nodeName, NODE_PER_PROVIDER_SUBSCRIPTION)) {
466                         throw new ParsingException("Unexpected NodeName: " + nodeName);
467                     }
468                     break;
469                 case TAG_NODE:
470                     // A node can be either an UpdateIdentifier node or a PerProviderSubscription
471                     // instance node.  Flatten out the XML tree first by converting it to a PPS
472                     // tree to reduce the complexity of the parsing code.
473                     PPSNode ppsNodeRoot = buildPpsNode(child);
474                     if (TextUtils.equals(ppsNodeRoot.getName(), NODE_UPDATE_IDENTIFIER)) {
475                         if (updateIdentifier != Integer.MIN_VALUE) {
476                             throw new ParsingException("Multiple node for UpdateIdentifier");
477                         }
478                         updateIdentifier = parseInteger(getPpsNodeValue(ppsNodeRoot));
479                     } else {
480                         // Only one PerProviderSubscription instance is expected and allowed.
481                         if (config != null) {
482                             throw new ParsingException("Multiple PPS instance");
483                         }
484                         config = parsePpsInstance(ppsNodeRoot);
485                     }
486                     break;
487                 case TAG_RT_PROPERTIES:
488                     // Parse and verify URN stored in the RT (Run Time) Properties.
489                     String urn = parseUrn(child);
490                     if (!TextUtils.equals(urn, PPS_MO_URN)) {
491                         throw new ParsingException("Unknown URN: " + urn);
492                     }
493                     break;
494                 default:
495                     throw new ParsingException("Unknown tag under PPS node: " + child.getTag());
496             }
497         }
498         if (config != null && updateIdentifier != Integer.MIN_VALUE) {
499             config.setUpdateIdentifier(updateIdentifier);
500         }
501         return config;
502     }
503 
504     /**
505      * Parse the URN stored in the RTProperties. Below is the format of the RTPProperties node:
506      *
507      * <RTProperties>
508      *   <Type>
509      *     <DDFName>urn:...</DDFName>
510      *   </Type>
511      * </RTProperties>
512      *
513      * @param node XMLNode that contains RTProperties node.
514      * @return URN String of URN.
515      * @throws ParsingException
516      */
parseUrn(XMLNode node)517     private static String parseUrn(XMLNode node) throws ParsingException {
518         if (node.getChildren().size() != 1)
519             throw new ParsingException("Expect RTPProperties node to only have one child");
520 
521         XMLNode typeNode = node.getChildren().get(0);
522         if (typeNode.getChildren().size() != 1) {
523             throw new ParsingException("Expect Type node to only have one child");
524         }
525         if (!TextUtils.equals(typeNode.getTag(), TAG_TYPE)) {
526             throw new ParsingException("Unexpected tag for Type: " + typeNode.getTag());
527         }
528 
529         XMLNode ddfNameNode = typeNode.getChildren().get(0);
530         if (!ddfNameNode.getChildren().isEmpty()) {
531             throw new ParsingException("Expect DDFName node to have no child");
532         }
533         if (!TextUtils.equals(ddfNameNode.getTag(), TAG_DDF_NAME)) {
534             throw new ParsingException("Unexpected tag for DDFName: " + ddfNameNode.getTag());
535         }
536 
537         return ddfNameNode.getText();
538     }
539 
540     /**
541      * Convert a XML tree represented by XMLNode to a PPS (PerProviderSubscription) instance tree
542      * represented by PPSNode.  This flattens out the XML tree to allow easier and cleaner parsing
543      * of the PPS configuration data.  Only three types of XML tag are expected: "NodeName",
544      * "Node", and "Value".
545      *
546      * The original XML tree (each XML element represent a node):
547      *
548      * <Node>
549      *   <NodeName>root</NodeName>
550      *   <Node>
551      *     <NodeName>child1</NodeName>
552      *     <Value>value1</Value>
553      *   </Node>
554      *   <Node>
555      *     <NodeName>child2</NodeName>
556      *     <Node>
557      *       <NodeName>grandchild1</NodeName>
558      *       ...
559      *     </Node>
560      *   </Node>
561      *   ...
562      * </Node>
563      *
564      * The converted PPS tree:
565      *
566      * [root] --- [child1, value1]
567      *   |
568      *   ---------[child2] --------[grandchild1] --- ...
569      *
570      * @param node XMLNode pointed to the root of a XML tree
571      * @return PPSNode pointing to the root of a PPS tree
572      * @throws ParsingException
573      */
buildPpsNode(XMLNode node)574     private static PPSNode buildPpsNode(XMLNode node) throws ParsingException {
575         String nodeName = null;
576         String nodeValue = null;
577         List<PPSNode> childNodes = new ArrayList<PPSNode>();
578         // Names of parsed child nodes, use for detecting multiple child nodes with the same name.
579         Set<String> parsedNodes = new HashSet<String>();
580 
581         for (XMLNode child : node.getChildren()) {
582             String tag = child.getTag();
583             if (TextUtils.equals(tag, TAG_NODE_NAME)) {
584                 if (nodeName != null) {
585                     throw new ParsingException("Duplicate NodeName node");
586                 }
587                 nodeName = child.getText();
588             } else if (TextUtils.equals(tag, TAG_NODE)) {
589                 PPSNode ppsNode = buildPpsNode(child);
590                 if (parsedNodes.contains(ppsNode.getName())) {
591                     throw new ParsingException("Duplicate node: " + ppsNode.getName());
592                 }
593                 parsedNodes.add(ppsNode.getName());
594                 childNodes.add(ppsNode);
595             } else if (TextUtils.equals(tag, TAG_VALUE)) {
596                if (nodeValue != null) {
597                    throw new ParsingException("Duplicate Value node");
598                }
599                nodeValue = child.getText();
600             } else {
601                 throw new ParsingException("Unknown tag: " + tag);
602             }
603         }
604 
605         if (nodeName == null) {
606             throw new ParsingException("Invalid node: missing NodeName");
607         }
608         if (nodeValue == null && childNodes.size() == 0) {
609             throw new ParsingException("Invalid node: " + nodeName +
610                     " missing both value and children");
611         }
612         if (nodeValue != null && childNodes.size() > 0) {
613             throw new ParsingException("Invalid node: " + nodeName +
614                     " contained both value and children");
615         }
616 
617         if (nodeValue != null) {
618             return new LeafNode(nodeName, nodeValue);
619         }
620         return new InternalNode(nodeName, childNodes);
621     }
622 
623     /**
624      * Return the value of a PPSNode.  An exception will be thrown if the given node
625      * is not a leaf node.
626      *
627      * @param node PPSNode to retrieve the value from
628      * @return String representing the value of the node
629      * @throws ParsingException
630      */
getPpsNodeValue(PPSNode node)631     private static String getPpsNodeValue(PPSNode node) throws ParsingException {
632         if (!node.isLeaf()) {
633             throw new ParsingException("Cannot get value from a non-leaf node: " + node.getName());
634         }
635         return node.getValue();
636     }
637 
638     /**
639      * Parse a PPS (PerProviderSubscription) configurations from a PPS tree.
640      *
641      * @param root PPSNode representing the root of the PPS tree
642      * @return PasspointConfiguration
643      * @throws ParsingException
644      */
parsePpsInstance(PPSNode root)645     private static PasspointConfiguration parsePpsInstance(PPSNode root)
646             throws ParsingException {
647         if (root.isLeaf()) {
648             throw new ParsingException("Leaf node not expected for PPS instance");
649         }
650 
651         PasspointConfiguration config = new PasspointConfiguration();
652         for (PPSNode child : root.getChildren()) {
653             switch(child.getName()) {
654                 case NODE_HOMESP:
655                     config.setHomeSp(parseHomeSP(child));
656                     break;
657                 case NODE_CREDENTIAL:
658                     config.setCredential(parseCredential(child));
659                     break;
660                 case NODE_POLICY:
661                     config.setPolicy(parsePolicy(child));
662                     break;
663                 case NODE_AAA_SERVER_TRUST_ROOT:
664                     config.setTrustRootCertList(parseAAAServerTrustRootList(child));
665                     break;
666                 case NODE_SUBSCRIPTION_UPDATE:
667                     config.setSubscriptionUpdate(parseUpdateParameter(child));
668                     break;
669                 case NODE_SUBSCRIPTION_PARAMETER:
670                     parseSubscriptionParameter(child, config);
671                     break;
672                 case NODE_CREDENTIAL_PRIORITY:
673                     config.setCredentialPriority(parseInteger(getPpsNodeValue(child)));
674                     break;
675                 case NODE_EXTENSION:
676                     // All vendor specific information will be under this node.
677                     parseExtension(child, config);
678                     break;
679                 default:
680                     throw new ParsingException("Unknown node: " + child.getName());
681             }
682         }
683         return config;
684     }
685 
686     /**
687      * Parse configurations under PerProviderSubscription/HomeSP subtree.
688      *
689      * @param node PPSNode representing the root of the PerProviderSubscription/HomeSP subtree
690      * @return HomeSP
691      * @throws ParsingException
692      */
parseHomeSP(PPSNode node)693     private static HomeSp parseHomeSP(PPSNode node) throws ParsingException {
694         if (node.isLeaf()) {
695             throw new ParsingException("Leaf node not expected for HomeSP");
696         }
697 
698         HomeSp homeSp = new HomeSp();
699         for (PPSNode child : node.getChildren()) {
700             switch (child.getName()) {
701                 case NODE_FQDN:
702                     homeSp.setFqdn(getPpsNodeValue(child));
703                     break;
704                 case NODE_FRIENDLY_NAME:
705                     homeSp.setFriendlyName(getPpsNodeValue(child));
706                     break;
707                 case NODE_ROAMING_CONSORTIUM_OI:
708                     homeSp.setRoamingConsortiumOis(
709                             parseRoamingConsortiumOI(getPpsNodeValue(child)));
710                     break;
711                 case NODE_ICON_URL:
712                     homeSp.setIconUrl(getPpsNodeValue(child));
713                     break;
714                 case NODE_NETWORK_ID:
715                     homeSp.setHomeNetworkIds(parseNetworkIds(child));
716                     break;
717                 case NODE_HOME_OI_LIST:
718                     Pair<List<Long>, List<Long>> homeOIs = parseHomeOIList(child);
719                     homeSp.setMatchAllOis(convertFromLongList(homeOIs.first));
720                     homeSp.setMatchAnyOis(convertFromLongList(homeOIs.second));
721                     break;
722                 case NODE_OTHER_HOME_PARTNERS:
723                     homeSp.setOtherHomePartners(parseOtherHomePartners(child));
724                     break;
725                 default:
726                     throw new ParsingException("Unknown node under HomeSP: " + child.getName());
727             }
728         }
729         return homeSp;
730     }
731 
732     /**
733      * Parse the roaming consortium OI string, which contains a list of OIs separated by ",".
734      *
735      * @param oiStr string containing list of OIs (Organization Identifiers) separated by ","
736      * @return long[]
737      * @throws ParsingException
738      */
parseRoamingConsortiumOI(String oiStr)739     private static long[] parseRoamingConsortiumOI(String oiStr)
740             throws ParsingException {
741         String[] oiStrArray = oiStr.split(",");
742         long[] oiArray = new long[oiStrArray.length];
743         for (int i = 0; i < oiStrArray.length; i++) {
744             oiArray[i] = parseLong(oiStrArray[i], 16);
745         }
746         return oiArray;
747     }
748 
749     /**
750      * Parse configurations under PerProviderSubscription/HomeSP/NetworkID subtree.
751      *
752      * @param node PPSNode representing the root of the PerProviderSubscription/HomeSP/NetworkID
753      *             subtree
754      * @return HashMap<String, Long> representing list of <SSID, HESSID> pair.
755      * @throws ParsingException
756      */
parseNetworkIds(PPSNode node)757     static private Map<String, Long> parseNetworkIds(PPSNode node)
758             throws ParsingException {
759         if (node.isLeaf()) {
760             throw new ParsingException("Leaf node not expected for NetworkID");
761         }
762 
763         Map<String, Long> networkIds = new HashMap<>();
764         for (PPSNode child : node.getChildren()) {
765             Pair<String, Long> networkId = parseNetworkIdInstance(child);
766             networkIds.put(networkId.first, networkId.second);
767         }
768         return networkIds;
769     }
770 
771     /**
772      * Parse configurations under PerProviderSubscription/HomeSP/NetworkID/<X+> subtree.
773      * The instance name (<X+>) is irrelevant and must be unique for each instance, which
774      * is verified when the PPS tree is constructed {@link #buildPpsNode}.
775      *
776      * @param node PPSNode representing the root of the
777      *             PerProviderSubscription/HomeSP/NetworkID/<X+> subtree
778      * @return Pair<String, Long> representing <SSID, HESSID> pair.
779      * @throws ParsingException
780      */
parseNetworkIdInstance(PPSNode node)781     static private Pair<String, Long> parseNetworkIdInstance(PPSNode node)
782             throws ParsingException {
783         if (node.isLeaf()) {
784             throw new ParsingException("Leaf node not expected for NetworkID instance");
785         }
786 
787         String ssid = null;
788         Long hessid = null;
789         for (PPSNode child : node.getChildren()) {
790             switch (child.getName()) {
791                 case NODE_SSID:
792                     ssid = getPpsNodeValue(child);
793                     break;
794                 case NODE_HESSID:
795                     hessid = parseLong(getPpsNodeValue(child), 16);
796                     break;
797                 default:
798                     throw new ParsingException("Unknown node under NetworkID instance: " +
799                             child.getName());
800             }
801         }
802         if (ssid == null)
803             throw new ParsingException("NetworkID instance missing SSID");
804 
805         return new Pair<String, Long>(ssid, hessid);
806     }
807 
808     /**
809      * Parse configurations under PerProviderSubscription/HomeSP/HomeOIList subtree.
810      *
811      * @param node PPSNode representing the root of the PerProviderSubscription/HomeSP/HomeOIList
812      *             subtree
813      * @return Pair<List<Long>, List<Long>> containing both MatchAllOIs and MatchAnyOIs list.
814      * @throws ParsingException
815      */
parseHomeOIList(PPSNode node)816     private static Pair<List<Long>, List<Long>> parseHomeOIList(PPSNode node)
817             throws ParsingException {
818         if (node.isLeaf()) {
819             throw new ParsingException("Leaf node not expected for HomeOIList");
820         }
821 
822         List<Long> matchAllOIs = new ArrayList<Long>();
823         List<Long> matchAnyOIs = new ArrayList<Long>();
824         for (PPSNode child : node.getChildren()) {
825             Pair<Long, Boolean> homeOI = parseHomeOIInstance(child);
826             if (homeOI.second.booleanValue()) {
827                 matchAllOIs.add(homeOI.first);
828             } else {
829                 matchAnyOIs.add(homeOI.first);
830             }
831         }
832         return new Pair<List<Long>, List<Long>>(matchAllOIs, matchAnyOIs);
833     }
834 
835     /**
836      * Parse configurations under PerProviderSubscription/HomeSP/HomeOIList/<X+> subtree.
837      * The instance name (<X+>) is irrelevant and must be unique for each instance, which
838      * is verified when the PPS tree is constructed {@link #buildPpsNode}.
839      *
840      * @param node PPSNode representing the root of the
841      *             PerProviderSubscription/HomeSP/HomeOIList/<X+> subtree
842      * @return Pair<Long, Boolean> containing a HomeOI and a HomeOIRequired flag
843      * @throws ParsingException
844      */
parseHomeOIInstance(PPSNode node)845     private static Pair<Long, Boolean> parseHomeOIInstance(PPSNode node) throws ParsingException {
846         if (node.isLeaf()) {
847             throw new ParsingException("Leaf node not expected for HomeOI instance");
848         }
849 
850         Long oi = null;
851         Boolean required = null;
852         for (PPSNode child : node.getChildren()) {
853             switch (child.getName()) {
854                 case NODE_HOME_OI:
855                     try {
856                         oi = Long.valueOf(getPpsNodeValue(child), 16);
857                     } catch (NumberFormatException e) {
858                         throw new ParsingException("Invalid HomeOI: " + getPpsNodeValue(child));
859                     }
860                     break;
861                 case NODE_HOME_OI_REQUIRED:
862                     required = Boolean.valueOf(getPpsNodeValue(child));
863                     break;
864                 default:
865                     throw new ParsingException("Unknown node under NetworkID instance: " +
866                             child.getName());
867             }
868         }
869         if (oi == null) {
870             throw new ParsingException("HomeOI instance missing OI field");
871         }
872         if (required == null) {
873             throw new ParsingException("HomeOI instance missing required field");
874         }
875         return new Pair<Long, Boolean>(oi, required);
876     }
877 
878     /**
879      * Parse configurations under PerProviderSubscription/HomeSP/OtherHomePartners subtree.
880      * This contains a list of FQDN (Fully Qualified Domain Name) that are considered
881      * home partners.
882      *
883      * @param node PPSNode representing the root of the
884      *             PerProviderSubscription/HomeSP/OtherHomePartners subtree
885      * @return String[] list of partner's FQDN
886      * @throws ParsingException
887      */
parseOtherHomePartners(PPSNode node)888     private static String[] parseOtherHomePartners(PPSNode node) throws ParsingException {
889         if (node.isLeaf()) {
890             throw new ParsingException("Leaf node not expected for OtherHomePartners");
891         }
892         List<String> otherHomePartners = new ArrayList<String>();
893         for (PPSNode child : node.getChildren()) {
894             String fqdn = parseOtherHomePartnerInstance(child);
895             otherHomePartners.add(fqdn);
896         }
897         return otherHomePartners.toArray(new String[otherHomePartners.size()]);
898     }
899 
900     /**
901      * Parse configurations under PerProviderSubscription/HomeSP/OtherHomePartners/<X+> subtree.
902      * The instance name (<X+>) is irrelevant and must be unique for each instance, which
903      * is verified when the PPS tree is constructed {@link #buildPpsNode}.
904      *
905      * @param node PPSNode representing the root of the
906      *             PerProviderSubscription/HomeSP/OtherHomePartners/<X+> subtree
907      * @return String FQDN of the partner
908      * @throws ParsingException
909      */
parseOtherHomePartnerInstance(PPSNode node)910     private static String parseOtherHomePartnerInstance(PPSNode node) throws ParsingException {
911         if (node.isLeaf()) {
912             throw new ParsingException("Leaf node not expected for OtherHomePartner instance");
913         }
914         String fqdn = null;
915         for (PPSNode child : node.getChildren()) {
916             switch (child.getName()) {
917                 case NODE_FQDN:
918                     fqdn = getPpsNodeValue(child);
919                     break;
920                 default:
921                     throw new ParsingException(
922                             "Unknown node under OtherHomePartner instance: " + child.getName());
923             }
924         }
925         if (fqdn == null) {
926             throw new ParsingException("OtherHomePartner instance missing FQDN field");
927         }
928         return fqdn;
929     }
930 
931     /**
932      * Parse configurations under PerProviderSubscription/Credential subtree.
933      *
934      * @param node PPSNode representing the root of the PerProviderSubscription/Credential subtree
935      * @return Credential
936      * @throws ParsingException
937      */
parseCredential(PPSNode node)938     private static Credential parseCredential(PPSNode node) throws ParsingException {
939         if (node.isLeaf()) {
940             throw new ParsingException("Leaf node not expected for Credential");
941         }
942 
943         Credential credential = new Credential();
944         for (PPSNode child: node.getChildren()) {
945             switch (child.getName()) {
946                 case NODE_CREATION_DATE:
947                     credential.setCreationTimeInMillis(parseDate(getPpsNodeValue(child)));
948                     break;
949                 case NODE_EXPIRATION_DATE:
950                     credential.setExpirationTimeInMillis(parseDate(getPpsNodeValue(child)));
951                     break;
952                 case NODE_USERNAME_PASSWORD:
953                     credential.setUserCredential(parseUserCredential(child));
954                     break;
955                 case NODE_DIGITAL_CERTIFICATE:
956                     credential.setCertCredential(parseCertificateCredential(child));
957                     break;
958                 case NODE_REALM:
959                     credential.setRealm(getPpsNodeValue(child));
960                     break;
961                 case NODE_CHECK_AAA_SERVER_CERT_STATUS:
962                     credential.setCheckAaaServerCertStatus(
963                             Boolean.parseBoolean(getPpsNodeValue(child)));
964                     break;
965                 case NODE_SIM:
966                     credential.setSimCredential(parseSimCredential(child));
967                     break;
968                 default:
969                     throw new ParsingException("Unknown node under Credential: " +
970                             child.getName());
971             }
972         }
973         return credential;
974     }
975 
976     /**
977      * Parse configurations under PerProviderSubscription/Credential/UsernamePassword subtree.
978      *
979      * @param node PPSNode representing the root of the
980      *             PerProviderSubscription/Credential/UsernamePassword subtree
981      * @return Credential.UserCredential
982      * @throws ParsingException
983      */
parseUserCredential(PPSNode node)984     private static Credential.UserCredential parseUserCredential(PPSNode node)
985             throws ParsingException {
986         if (node.isLeaf()) {
987             throw new ParsingException("Leaf node not expected for UsernamePassword");
988         }
989 
990         Credential.UserCredential userCred = new Credential.UserCredential();
991         for (PPSNode child : node.getChildren()) {
992             switch (child.getName()) {
993                 case NODE_USERNAME:
994                     userCred.setUsername(getPpsNodeValue(child));
995                     break;
996                 case NODE_PASSWORD:
997                     userCred.setPassword(getPpsNodeValue(child));
998                     break;
999                 case NODE_MACHINE_MANAGED:
1000                     userCred.setMachineManaged(Boolean.parseBoolean(getPpsNodeValue(child)));
1001                     break;
1002                 case NODE_SOFT_TOKEN_APP:
1003                     userCred.setSoftTokenApp(getPpsNodeValue(child));
1004                     break;
1005                 case NODE_ABLE_TO_SHARE:
1006                     userCred.setAbleToShare(Boolean.parseBoolean(getPpsNodeValue(child)));
1007                     break;
1008                 case NODE_EAP_METHOD:
1009                     parseEAPMethod(child, userCred);
1010                     break;
1011                 default:
1012                     throw new ParsingException("Unknown node under UsernamePassword: "
1013                             + child.getName());
1014             }
1015         }
1016         return userCred;
1017     }
1018 
1019     /**
1020      * Parse configurations under PerProviderSubscription/Credential/UsernamePassword/EAPMethod
1021      * subtree.
1022      *
1023      * @param node PPSNode representing the root of the
1024      *             PerProviderSubscription/Credential/UsernamePassword/EAPMethod subtree
1025      * @param userCred UserCredential to be updated with EAP method values.
1026      * @throws ParsingException
1027      */
parseEAPMethod(PPSNode node, Credential.UserCredential userCred)1028     private static void parseEAPMethod(PPSNode node, Credential.UserCredential userCred)
1029             throws ParsingException {
1030         if (node.isLeaf()) {
1031             throw new ParsingException("Leaf node not expected for EAPMethod");
1032         }
1033 
1034         for (PPSNode child : node.getChildren()) {
1035             switch(child.getName()) {
1036                 case NODE_EAP_TYPE:
1037                     userCred.setEapType(parseInteger(getPpsNodeValue(child)));
1038                     break;
1039                 case NODE_INNER_METHOD:
1040                     userCred.setNonEapInnerMethod(getPpsNodeValue(child));
1041                     break;
1042                 case NODE_VENDOR_ID:
1043                 case NODE_VENDOR_TYPE:
1044                 case NODE_INNER_EAP_TYPE:
1045                 case NODE_INNER_VENDOR_ID:
1046                 case NODE_INNER_VENDOR_TYPE:
1047                     // Only EAP-TTLS is currently supported for user credential, which doesn't
1048                     // use any of these parameters.
1049                     Log.d(TAG, "Ignore unsupported EAP method parameter: " + child.getName());
1050                     break;
1051                 default:
1052                     throw new ParsingException("Unknown node under EAPMethod: " + child.getName());
1053             }
1054         }
1055     }
1056 
1057     /**
1058      * Parse configurations under PerProviderSubscription/Credential/DigitalCertificate subtree.
1059      *
1060      * @param node PPSNode representing the root of the
1061      *             PerProviderSubscription/Credential/DigitalCertificate subtree
1062      * @return Credential.CertificateCredential
1063      * @throws ParsingException
1064      */
parseCertificateCredential(PPSNode node)1065     private static Credential.CertificateCredential parseCertificateCredential(PPSNode node)
1066             throws ParsingException {
1067         if (node.isLeaf()) {
1068             throw new ParsingException("Leaf node not expected for CertificateCredential");
1069         }
1070 
1071         Credential.CertificateCredential certCred = new Credential.CertificateCredential();
1072         for (PPSNode child : node.getChildren()) {
1073             switch (child.getName()) {
1074                 case NODE_CERTIFICATE_TYPE:
1075                     certCred.setCertType(getPpsNodeValue(child));
1076                     break;
1077                 case NODE_CERT_SHA256_FINGERPRINT:
1078                     certCred.setCertSha256Fingerprint(parseHexString(getPpsNodeValue(child)));
1079                     break;
1080                 default:
1081                     throw new ParsingException("Unknown node under CertificateCredential: "
1082                             + child.getName());
1083             }
1084         }
1085         return certCred;
1086     }
1087 
1088     /**
1089      * Parse configurations under PerProviderSubscription/Credential/SIM subtree.
1090      *
1091      * @param node PPSNode representing the root of the PerProviderSubscription/Credential/SIM
1092      *             subtree
1093      * @return Credential.SimCredential
1094      * @throws ParsingException
1095      */
parseSimCredential(PPSNode node)1096     private static Credential.SimCredential parseSimCredential(PPSNode node)
1097             throws ParsingException {
1098         if (node.isLeaf()) {
1099             throw new ParsingException("Leaf node not expected for SimCredential");
1100         }
1101 
1102         Credential.SimCredential simCred = new Credential.SimCredential();
1103         for (PPSNode child : node.getChildren()) {
1104             switch (child.getName()) {
1105                 case NODE_SIM_IMSI:
1106                     simCred.setImsi(getPpsNodeValue(child));
1107                     break;
1108                 case NODE_EAP_TYPE:
1109                     simCred.setEapType(parseInteger(getPpsNodeValue(child)));
1110                     break;
1111                 default:
1112                     throw new ParsingException("Unknown node under SimCredential: "
1113                             + child.getName());
1114             }
1115         }
1116         return simCred;
1117     }
1118 
1119     /**
1120      * Parse configurations under PerProviderSubscription/Policy subtree.
1121      *
1122      * @param node PPSNode representing the root of the PerProviderSubscription/Policy subtree
1123      * @return {@link Policy}
1124      * @throws ParsingException
1125      */
parsePolicy(PPSNode node)1126     private static Policy parsePolicy(PPSNode node) throws ParsingException {
1127         if (node.isLeaf()) {
1128             throw new ParsingException("Leaf node not expected for Policy");
1129         }
1130 
1131         Policy policy = new Policy();
1132         for (PPSNode child : node.getChildren()) {
1133             switch (child.getName()) {
1134                 case NODE_PREFERRED_ROAMING_PARTNER_LIST:
1135                     policy.setPreferredRoamingPartnerList(parsePreferredRoamingPartnerList(child));
1136                     break;
1137                 case NODE_MIN_BACKHAUL_THRESHOLD:
1138                     parseMinBackhaulThreshold(child, policy);
1139                     break;
1140                 case NODE_POLICY_UPDATE:
1141                     policy.setPolicyUpdate(parseUpdateParameter(child));
1142                     break;
1143                 case NODE_SP_EXCLUSION_LIST:
1144                     policy.setExcludedSsidList(parseSpExclusionList(child));
1145                     break;
1146                 case NODE_REQUIRED_PROTO_PORT_TUPLE:
1147                     policy.setRequiredProtoPortMap(parseRequiredProtoPortTuple(child));
1148                     break;
1149                 case NODE_MAXIMUM_BSS_LOAD_VALUE:
1150                     policy.setMaximumBssLoadValue(parseInteger(getPpsNodeValue(child)));
1151                     break;
1152                 default:
1153                     throw new ParsingException("Unknown node under Policy: " + child.getName());
1154             }
1155         }
1156         return policy;
1157     }
1158 
1159     /**
1160      * Parse configurations under PerProviderSubscription/Policy/PreferredRoamingPartnerList
1161      * subtree.
1162      *
1163      * @param node PPSNode representing the root of the
1164      *             PerProviderSubscription/Policy/PreferredRoamingPartnerList subtree
1165      * @return List of {@link Policy#RoamingPartner}
1166      * @throws ParsingException
1167      */
parsePreferredRoamingPartnerList(PPSNode node)1168     private static List<Policy.RoamingPartner> parsePreferredRoamingPartnerList(PPSNode node)
1169             throws ParsingException {
1170         if (node.isLeaf()) {
1171             throw new ParsingException("Leaf node not expected for PreferredRoamingPartnerList");
1172         }
1173         List<Policy.RoamingPartner> partnerList = new ArrayList<>();
1174         for (PPSNode child : node.getChildren()) {
1175             partnerList.add(parsePreferredRoamingPartner(child));
1176         }
1177         return partnerList;
1178     }
1179 
1180     /**
1181      * Parse configurations under PerProviderSubscription/Policy/PreferredRoamingPartnerList/<X+>
1182      * subtree.
1183      *
1184      * @param node PPSNode representing the root of the
1185      *             PerProviderSubscription/Policy/PreferredRoamingPartnerList/<X+> subtree
1186      * @return {@link Policy#RoamingPartner}
1187      * @throws ParsingException
1188      */
parsePreferredRoamingPartner(PPSNode node)1189     private static Policy.RoamingPartner parsePreferredRoamingPartner(PPSNode node)
1190             throws ParsingException {
1191         if (node.isLeaf()) {
1192             throw new ParsingException("Leaf node not expected for PreferredRoamingPartner "
1193                     + "instance");
1194         }
1195 
1196         Policy.RoamingPartner roamingPartner = new Policy.RoamingPartner();
1197         for (PPSNode child : node.getChildren()) {
1198             switch (child.getName()) {
1199                 case NODE_FQDN_MATCH:
1200                     // FQDN_Match field is in the format of "[FQDN],[MatchInfo]", where [MatchInfo]
1201                     // is either "exactMatch" for exact match of FQDN or "includeSubdomains" for
1202                     // matching all FQDNs with the same sub-domain.
1203                     String fqdnMatch = getPpsNodeValue(child);
1204                     String[] fqdnMatchArray = fqdnMatch.split(",");
1205                     if (fqdnMatchArray.length != 2) {
1206                         throw new ParsingException("Invalid FQDN_Match: " + fqdnMatch);
1207                     }
1208                     roamingPartner.setFqdn(fqdnMatchArray[0]);
1209                     if (TextUtils.equals(fqdnMatchArray[1], "exactMatch")) {
1210                         roamingPartner.setFqdnExactMatch(true);
1211                     } else if (TextUtils.equals(fqdnMatchArray[1], "includeSubdomains")) {
1212                         roamingPartner.setFqdnExactMatch(false);
1213                     } else {
1214                         throw new ParsingException("Invalid FQDN_Match: " + fqdnMatch);
1215                     }
1216                     break;
1217                 case NODE_PRIORITY:
1218                     roamingPartner.setPriority(parseInteger(getPpsNodeValue(child)));
1219                     break;
1220                 case NODE_COUNTRY:
1221                     roamingPartner.setCountries(getPpsNodeValue(child));
1222                     break;
1223                 default:
1224                     throw new ParsingException("Unknown node under PreferredRoamingPartnerList "
1225                             + "instance " + child.getName());
1226             }
1227         }
1228         return roamingPartner;
1229     }
1230 
1231     /**
1232      * Parse configurations under PerProviderSubscription/Policy/MinBackhaulThreshold subtree
1233      * into the given policy.
1234      *
1235      * @param node PPSNode representing the root of the
1236      *             PerProviderSubscription/Policy/MinBackhaulThreshold subtree
1237      * @param policy The policy to store the MinBackhualThreshold configuration
1238      * @throws ParsingException
1239      */
parseMinBackhaulThreshold(PPSNode node, Policy policy)1240     private static void parseMinBackhaulThreshold(PPSNode node, Policy policy)
1241             throws ParsingException {
1242         if (node.isLeaf()) {
1243             throw new ParsingException("Leaf node not expected for MinBackhaulThreshold");
1244         }
1245         for (PPSNode child : node.getChildren()) {
1246             parseMinBackhaulThresholdInstance(child, policy);
1247         }
1248     }
1249 
1250     /**
1251      * Parse configurations under PerProviderSubscription/Policy/MinBackhaulThreshold/<X+> subtree
1252      * into the given policy.
1253      *
1254      * @param node PPSNode representing the root of the
1255      *             PerProviderSubscription/Policy/MinBackhaulThreshold/<X+> subtree
1256      * @param policy The policy to store the MinBackhaulThreshold configuration
1257      * @throws ParsingException
1258      */
parseMinBackhaulThresholdInstance(PPSNode node, Policy policy)1259     private static void parseMinBackhaulThresholdInstance(PPSNode node, Policy policy)
1260             throws ParsingException {
1261         if (node.isLeaf()) {
1262             throw new ParsingException("Leaf node not expected for MinBackhaulThreshold instance");
1263         }
1264         String networkType = null;
1265         long downlinkBandwidth = Long.MIN_VALUE;
1266         long uplinkBandwidth = Long.MIN_VALUE;
1267         for (PPSNode child : node.getChildren()) {
1268             switch (child.getName()) {
1269                 case NODE_NETWORK_TYPE:
1270                     networkType = getPpsNodeValue(child);
1271                     break;
1272                 case NODE_DOWNLINK_BANDWIDTH:
1273                     downlinkBandwidth = parseLong(getPpsNodeValue(child), 10);
1274                     break;
1275                 case NODE_UPLINK_BANDWIDTH:
1276                     uplinkBandwidth = parseLong(getPpsNodeValue(child), 10);
1277                     break;
1278                 default:
1279                     throw new ParsingException("Unknown node under MinBackhaulThreshold instance "
1280                             + child.getName());
1281             }
1282         }
1283         if (networkType == null) {
1284             throw new ParsingException("Missing NetworkType field");
1285         }
1286 
1287         if (TextUtils.equals(networkType, "home")) {
1288             policy.setMinHomeDownlinkBandwidth(downlinkBandwidth);
1289             policy.setMinHomeUplinkBandwidth(uplinkBandwidth);
1290         } else if (TextUtils.equals(networkType, "roaming")) {
1291             policy.setMinRoamingDownlinkBandwidth(downlinkBandwidth);
1292             policy.setMinRoamingUplinkBandwidth(uplinkBandwidth);
1293         } else {
1294             throw new ParsingException("Invalid network type: " + networkType);
1295         }
1296     }
1297 
1298     /**
1299      * Parse update parameters. This contained configurations from either
1300      * PerProviderSubscription/Policy/PolicyUpdate or PerProviderSubscription/SubscriptionUpdate
1301      * subtree.
1302      *
1303      * @param node PPSNode representing the root of the PerProviderSubscription/Policy/PolicyUpdate
1304      *             or PerProviderSubscription/SubscriptionUpdate subtree
1305      * @return {@link UpdateParameter}
1306      * @throws ParsingException
1307      */
parseUpdateParameter(PPSNode node)1308     private static UpdateParameter parseUpdateParameter(PPSNode node)
1309             throws ParsingException {
1310         if (node.isLeaf()) {
1311             throw new ParsingException("Leaf node not expected for Update Parameters");
1312         }
1313 
1314         UpdateParameter updateParam = new UpdateParameter();
1315         for (PPSNode child : node.getChildren()) {
1316             switch(child.getName()) {
1317                 case NODE_UPDATE_INTERVAL:
1318                     updateParam.setUpdateIntervalInMinutes(parseLong(getPpsNodeValue(child), 10));
1319                     break;
1320                 case NODE_UPDATE_METHOD:
1321                     updateParam.setUpdateMethod(getPpsNodeValue(child));
1322                     break;
1323                 case NODE_RESTRICTION:
1324                     updateParam.setRestriction(getPpsNodeValue(child));
1325                     break;
1326                 case NODE_URI:
1327                     updateParam.setServerUri(getPpsNodeValue(child));
1328                     break;
1329                 case NODE_USERNAME_PASSWORD:
1330                     Pair<String, String> usernamePassword = parseUpdateUserCredential(child);
1331                     updateParam.setUsername(usernamePassword.first);
1332                     updateParam.setBase64EncodedPassword(usernamePassword.second);
1333                     break;
1334                 case NODE_TRUST_ROOT:
1335                     Pair<String, byte[]> trustRoot = parseTrustRoot(child);
1336                     updateParam.setTrustRootCertUrl(trustRoot.first);
1337                     updateParam.setTrustRootCertSha256Fingerprint(trustRoot.second);
1338                     break;
1339                 case NODE_OTHER:
1340                     Log.d(TAG, "Ignore unsupported paramter: " + child.getName());
1341                     break;
1342                 default:
1343                     throw new ParsingException("Unknown node under Update Parameters: "
1344                             + child.getName());
1345             }
1346         }
1347         return updateParam;
1348     }
1349 
1350     /**
1351      * Parse username and password parameters associated with policy or subscription update.
1352      * This contained configurations under either
1353      * PerProviderSubscription/Policy/PolicyUpdate/UsernamePassword or
1354      * PerProviderSubscription/SubscriptionUpdate/UsernamePassword subtree.
1355      *
1356      * @param node PPSNode representing the root of the UsernamePassword subtree
1357      * @return Pair of username and password
1358      * @throws ParsingException
1359      */
parseUpdateUserCredential(PPSNode node)1360     private static Pair<String, String> parseUpdateUserCredential(PPSNode node)
1361             throws ParsingException {
1362         if (node.isLeaf()) {
1363             throw new ParsingException("Leaf node not expected for UsernamePassword");
1364         }
1365 
1366         String username = null;
1367         String password = null;
1368         for (PPSNode child : node.getChildren()) {
1369             switch (child.getName()) {
1370                 case NODE_USERNAME:
1371                     username = getPpsNodeValue(child);
1372                     break;
1373                 case NODE_PASSWORD:
1374                     password = getPpsNodeValue(child);
1375                     break;
1376                 default:
1377                     throw new ParsingException("Unknown node under UsernamePassword: "
1378                             + child.getName());
1379             }
1380         }
1381         return Pair.create(username, password);
1382     }
1383 
1384     /**
1385      * Parse the trust root parameters associated with policy update, subscription update, or AAA
1386      * server trust root.
1387      *
1388      * This contained configurations under either
1389      * PerProviderSubscription/Policy/PolicyUpdate/TrustRoot or
1390      * PerProviderSubscription/SubscriptionUpdate/TrustRoot or
1391      * PerProviderSubscription/AAAServerTrustRoot/<X+> subtree.
1392      *
1393      * @param node PPSNode representing the root of the TrustRoot subtree
1394      * @return Pair of Certificate URL and fingerprint
1395      * @throws ParsingException
1396      */
parseTrustRoot(PPSNode node)1397     private static Pair<String, byte[]> parseTrustRoot(PPSNode node)
1398             throws ParsingException {
1399         if (node.isLeaf()) {
1400             throw new ParsingException("Leaf node not expected for TrustRoot");
1401         }
1402 
1403         String certUrl = null;
1404         byte[] certFingerprint = null;
1405         for (PPSNode child : node.getChildren()) {
1406             switch (child.getName()) {
1407                 case NODE_CERT_URL:
1408                     certUrl = getPpsNodeValue(child);
1409                     break;
1410                 case NODE_CERT_SHA256_FINGERPRINT:
1411                     certFingerprint = parseHexString(getPpsNodeValue(child));
1412                     break;
1413                 default:
1414                     throw new ParsingException("Unknown node under TrustRoot: "
1415                             + child.getName());
1416             }
1417         }
1418         return Pair.create(certUrl, certFingerprint);
1419     }
1420 
1421     /**
1422      * Parse configurations under PerProviderSubscription/Policy/SPExclusionList subtree.
1423      *
1424      * @param node PPSNode representing the root of the
1425      *             PerProviderSubscription/Policy/SPExclusionList subtree
1426      * @return Array of excluded SSIDs
1427      * @throws ParsingException
1428      */
parseSpExclusionList(PPSNode node)1429     private static String[] parseSpExclusionList(PPSNode node) throws ParsingException {
1430         if (node.isLeaf()) {
1431             throw new ParsingException("Leaf node not expected for SPExclusionList");
1432         }
1433         List<String> ssidList = new ArrayList<>();
1434         for (PPSNode child : node.getChildren()) {
1435             ssidList.add(parseSpExclusionInstance(child));
1436         }
1437         return ssidList.toArray(new String[ssidList.size()]);
1438     }
1439 
1440     /**
1441      * Parse configurations under PerProviderSubscription/Policy/SPExclusionList/<X+> subtree.
1442      *
1443      * @param node PPSNode representing the root of the
1444      *             PerProviderSubscription/Policy/SPExclusionList/<X+> subtree
1445      * @return String
1446      * @throws ParsingException
1447      */
parseSpExclusionInstance(PPSNode node)1448     private static String parseSpExclusionInstance(PPSNode node) throws ParsingException {
1449         if (node.isLeaf()) {
1450             throw new ParsingException("Leaf node not expected for SPExclusion instance");
1451         }
1452         String ssid = null;
1453         for (PPSNode child : node.getChildren()) {
1454             switch (child.getName()) {
1455                 case NODE_SSID:
1456                     ssid = getPpsNodeValue(child);
1457                     break;
1458                 default:
1459                     throw new ParsingException("Unknown node under SPExclusion instance");
1460             }
1461         }
1462         return ssid;
1463     }
1464 
1465     /**
1466      * Parse configurations under PerProviderSubscription/Policy/RequiredProtoPortTuple subtree.
1467      *
1468      * @param node PPSNode representing the root of the
1469      *             PerProviderSubscription/Policy/RequiredProtoPortTuple subtree
1470      * @return Map of IP Protocol to Port Number tuples
1471      * @throws ParsingException
1472      */
parseRequiredProtoPortTuple(PPSNode node)1473     private static Map<Integer, String> parseRequiredProtoPortTuple(PPSNode node)
1474             throws ParsingException {
1475         if (node.isLeaf()) {
1476             throw new ParsingException("Leaf node not expected for RequiredProtoPortTuple");
1477         }
1478         Map<Integer, String> protoPortTupleMap = new HashMap<>();
1479         for (PPSNode child : node.getChildren()) {
1480             Pair<Integer, String> protoPortTuple = parseProtoPortTuple(child);
1481             protoPortTupleMap.put(protoPortTuple.first, protoPortTuple.second);
1482         }
1483         return protoPortTupleMap;
1484     }
1485 
1486     /**
1487      * Parse configurations under PerProviderSubscription/Policy/RequiredProtoPortTuple/<X+>
1488      * subtree.
1489      *
1490      * @param node PPSNode representing the root of the
1491      *             PerProviderSubscription/Policy/RequiredProtoPortTuple/<X+> subtree
1492      * @return Pair of IP Protocol to Port Number tuple
1493      * @throws ParsingException
1494      */
parseProtoPortTuple(PPSNode node)1495     private static Pair<Integer, String> parseProtoPortTuple(PPSNode node)
1496             throws ParsingException {
1497         if (node.isLeaf()) {
1498             throw new ParsingException("Leaf node not expected for RequiredProtoPortTuple "
1499                     + "instance");
1500         }
1501         int proto = Integer.MIN_VALUE;
1502         String ports = null;
1503         for (PPSNode child : node.getChildren()) {
1504             switch (child.getName()) {
1505                 case NODE_IP_PROTOCOL:
1506                     proto = parseInteger(getPpsNodeValue(child));
1507                     break;
1508                 case NODE_PORT_NUMBER:
1509                     ports = getPpsNodeValue(child);
1510                     break;
1511                 default:
1512                     throw new ParsingException("Unknown node under RequiredProtoPortTuple instance"
1513                             + child.getName());
1514             }
1515         }
1516         if (proto == Integer.MIN_VALUE) {
1517             throw new ParsingException("Missing IPProtocol field");
1518         }
1519         if (ports == null) {
1520             throw new ParsingException("Missing PortNumber field");
1521         }
1522         return Pair.create(proto, ports);
1523     }
1524 
1525     /**
1526      * Parse configurations under PerProviderSubscription/AAAServerTrustRoot subtree.
1527      *
1528      * @param node PPSNode representing the root of PerProviderSubscription/AAAServerTrustRoot
1529      *             subtree
1530      * @return Map of certificate URL with the corresponding certificate fingerprint
1531      * @throws ParsingException
1532      */
parseAAAServerTrustRootList(PPSNode node)1533     private static Map<String, byte[]> parseAAAServerTrustRootList(PPSNode node)
1534             throws ParsingException {
1535         if (node.isLeaf()) {
1536             throw new ParsingException("Leaf node not expected for AAAServerTrustRoot");
1537         }
1538         Map<String, byte[]> certList = new HashMap<>();
1539         for (PPSNode child : node.getChildren()) {
1540             Pair<String, byte[]> certTuple = parseTrustRoot(child);
1541             certList.put(certTuple.first, certTuple.second);
1542         }
1543         return certList;
1544     }
1545 
1546     /**
1547      * Parse configurations under PerProviderSubscription/SubscriptionParameter subtree.
1548      *
1549      * @param node PPSNode representing the root of PerProviderSubscription/SubscriptionParameter
1550      *             subtree
1551      * @param config Instance of {@link PasspointConfiguration}
1552      * @throws ParsingException
1553      */
parseSubscriptionParameter(PPSNode node, PasspointConfiguration config)1554     private static void parseSubscriptionParameter(PPSNode node, PasspointConfiguration config)
1555             throws ParsingException {
1556         if (node.isLeaf()) {
1557             throw new ParsingException("Leaf node not expected for SubscriptionParameter");
1558         }
1559         for (PPSNode child : node.getChildren()) {
1560             switch (child.getName()) {
1561                 case NODE_CREATION_DATE:
1562                     config.setSubscriptionCreationTimeInMillis(parseDate(getPpsNodeValue(child)));
1563                     break;
1564                 case NODE_EXPIRATION_DATE:
1565                     config.setSubscriptionExpirationTimeInMillis(parseDate(getPpsNodeValue(child)));
1566                     break;
1567                 case NODE_TYPE_OF_SUBSCRIPTION:
1568                     config.setSubscriptionType(getPpsNodeValue(child));
1569                     break;
1570                 case NODE_USAGE_LIMITS:
1571                     parseUsageLimits(child, config);
1572                     break;
1573                 default:
1574                     throw new ParsingException("Unknown node under SubscriptionParameter"
1575                             + child.getName());
1576             }
1577         }
1578     }
1579 
1580     /**
1581      * Parse configurations under PerProviderSubscription/SubscriptionParameter/UsageLimits
1582      * subtree.
1583      *
1584      * @param node PPSNode representing the root of
1585      *             PerProviderSubscription/SubscriptionParameter/UsageLimits subtree
1586      * @param config Instance of {@link PasspointConfiguration}
1587      * @throws ParsingException
1588      */
parseUsageLimits(PPSNode node, PasspointConfiguration config)1589     private static void parseUsageLimits(PPSNode node, PasspointConfiguration config)
1590             throws ParsingException {
1591         if (node.isLeaf()) {
1592             throw new ParsingException("Leaf node not expected for UsageLimits");
1593         }
1594         for (PPSNode child : node.getChildren()) {
1595             switch (child.getName()) {
1596                 case NODE_DATA_LIMIT:
1597                     config.setUsageLimitDataLimit(parseLong(getPpsNodeValue(child), 10));
1598                     break;
1599                 case NODE_START_DATE:
1600                     config.setUsageLimitStartTimeInMillis(parseDate(getPpsNodeValue(child)));
1601                     break;
1602                 case NODE_TIME_LIMIT:
1603                     config.setUsageLimitTimeLimitInMinutes(parseLong(getPpsNodeValue(child), 10));
1604                     break;
1605                 case NODE_USAGE_TIME_PERIOD:
1606                     config.setUsageLimitUsageTimePeriodInMinutes(
1607                             parseLong(getPpsNodeValue(child), 10));
1608                     break;
1609                 default:
1610                     throw new ParsingException("Unknown node under UsageLimits"
1611                             + child.getName());
1612             }
1613         }
1614     }
1615 
1616     /**
1617      * Parse configurations under PerProviderSubscription/Extension/Android/AAAServerTrustedNames
1618      * subtree.
1619      *
1620      * @param node PPSNode representing the root of the
1621      *             PerProviderSubscription/Extension/Android/AAAServerTrustedNames subtree
1622      * @return String[] list of trusted name
1623      * @throws ParsingException
1624      */
parseAaaServerTrustedNames(PPSNode node)1625     private static String[] parseAaaServerTrustedNames(PPSNode node) throws ParsingException {
1626         if (node.isLeaf()) {
1627             throw new ParsingException("Leaf node not expected for AAAServerTrustedNames instance");
1628         }
1629         String fqdnListStr = null;
1630         String[] fqdnListArray = null;
1631         for (PPSNode child : node.getChildren()) {
1632             switch (child.getName()) {
1633                 case NODE_FQDN:
1634                     fqdnListStr = getPpsNodeValue(child);
1635                     fqdnListArray = fqdnListStr.split(";");
1636                     break;
1637                 default:
1638                     throw new ParsingException(
1639                             "Unknown node under AAAServerTrustedNames instance: "
1640                             + child.getName());
1641             }
1642         }
1643         if (fqdnListArray == null) {
1644             throw new ParsingException("AAAServerTrustedNames instance missing FQDN field");
1645         }
1646 
1647         return fqdnListArray;
1648     }
1649 
1650     /**
1651      * Parse configurations under PerProviderSubscription/Extension/Android subtree.
1652      *
1653      * @param node PPSNode representing the root of PerProviderSubscription/Extension
1654      *             subtree
1655      * @param config Instance of {@link PasspointConfiguration}
1656      * @throws ParsingException
1657      */
parseVendorAndroidExtension(PPSNode node, PasspointConfiguration config)1658     private static void parseVendorAndroidExtension(PPSNode node, PasspointConfiguration config)
1659             throws ParsingException {
1660         if (node.isLeaf()) {
1661             throw new ParsingException("Leaf node not expected for AndroidExtension");
1662         }
1663         for (PPSNode child : node.getChildren()) {
1664             switch (child.getName()) {
1665                 case NODE_AAA_SERVER_TRUSTED_NAMES:
1666                     config.setAaaServerTrustedNames(parseAaaServerTrustedNames(child));
1667                     break;
1668                 default:
1669                     // Don't raise an exception for unknown nodes to avoid breaking old release
1670                     Log.w(TAG, "Unknown node under Android Extension: " + child.getName());
1671             }
1672         }
1673     }
1674 
1675     /**
1676      * Parse configurations under PerProviderSubscription/Extension subtree.
1677      *
1678      * @param node PPSNode representing the root of PerProviderSubscription/Extension
1679      *             subtree
1680      * @param config Instance of {@link PasspointConfiguration}
1681      * @throws ParsingException
1682      */
parseExtension(PPSNode node, PasspointConfiguration config)1683     private static void parseExtension(PPSNode node, PasspointConfiguration config)
1684             throws ParsingException {
1685         if (node.isLeaf()) {
1686             throw new ParsingException("Leaf node not expected for Extension");
1687         }
1688         for (PPSNode child : node.getChildren()) {
1689             switch (child.getName()) {
1690                 case NODE_VENDOR_ANDROID:
1691                     parseVendorAndroidExtension(child, config);
1692                     break;
1693                 default:
1694                     // Unknown nodes under Extension won't raise exception.
1695                     // This allows adding new nodes in the future and
1696                     // won't break older release.
1697                     Log.w(TAG, "Unknown node under Extension: " + child.getName());
1698             }
1699         }
1700     }
1701 
1702     /**
1703      * Convert a hex string to a byte array.
1704      *
1705      * @param str String containing hex values
1706      * @return byte[]
1707      * @throws ParsingException
1708      */
parseHexString(String str)1709     private static byte[] parseHexString(String str) throws ParsingException {
1710         if ((str.length() & 1) == 1) {
1711             throw new ParsingException("Odd length hex string: " + str + ", length: "
1712                     + str.length());
1713         }
1714 
1715         byte[] result = new byte[str.length() / 2];
1716         for (int i = 0; i < result.length; i++) {
1717           int index = i * 2;
1718           try {
1719               result[i] = (byte) Integer.parseInt(str.substring(index, index + 2), 16);
1720           } catch (NumberFormatException e) {
1721               throw new ParsingException("Invalid hex string: " + str);
1722           }
1723         }
1724         return result;
1725     }
1726 
1727     /**
1728      * Convert a date string to the number of milliseconds since January 1, 1970, 00:00:00 GMT.
1729      *
1730      * @param dateStr String in the format of yyyy-MM-dd'T'HH:mm:ss'Z'
1731      * @return number of milliseconds
1732      * @throws ParsingException
1733      */
parseDate(String dateStr)1734     private static long parseDate(String dateStr) throws ParsingException {
1735         try {
1736             DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
1737             return format.parse(dateStr).getTime();
1738         } catch (ParseException pe) {
1739             throw new ParsingException("Badly formatted time: " + dateStr);
1740         }
1741     }
1742 
1743     /**
1744      * Parse an integer string.
1745      *
1746      * @param value String of integer value
1747      * @return int
1748      * @throws ParsingException
1749      */
parseInteger(String value)1750     private static int parseInteger(String value) throws ParsingException {
1751         try {
1752             return Integer.parseInt(value);
1753         } catch (NumberFormatException e) {
1754             throw new ParsingException("Invalid integer value: " + value);
1755         }
1756     }
1757 
1758     /**
1759      * Parse a string representing a long integer.
1760      *
1761      * @param value String of long integer value
1762      * @return long
1763      * @throws ParsingException
1764      */
parseLong(String value, int radix)1765     private static long parseLong(String value, int radix) throws ParsingException {
1766         try {
1767             return Long.parseLong(value, radix);
1768         } catch (NumberFormatException e) {
1769             throw new ParsingException("Invalid long integer value: " + value);
1770         }
1771     }
1772 
1773     /**
1774      * Convert a List<Long> to a primitive long array long[].
1775      *
1776      * @param list List to be converted
1777      * @return long[]
1778      */
convertFromLongList(List<Long> list)1779     private static long[] convertFromLongList(List<Long> list) {
1780         Long[] objectArray = list.toArray(new Long[list.size()]);
1781         long[] primitiveArray = new long[objectArray.length];
1782         for (int i = 0; i < objectArray.length; i++) {
1783             primitiveArray[i] = objectArray[i].longValue();
1784         }
1785         return primitiveArray;
1786     }
1787 }
1788