Home | History | Annotate | Download | only in hotspot2
      1 /*
      2  * Copyright (C) 2017 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 com.android.server.wifi.hotspot2;
     18 
     19 import android.text.TextUtils;
     20 import android.util.Log;
     21 import android.util.Pair;
     22 
     23 import java.io.BufferedReader;
     24 import java.io.FileReader;
     25 import java.io.IOException;
     26 import java.nio.charset.StandardCharsets;
     27 import java.util.ArrayList;
     28 import java.util.HashMap;
     29 import java.util.List;
     30 import java.util.Map;
     31 
     32 /**
     33  * Utility class for parsing legacy (N and older) Passpoint configuration file content
     34  * (/data/misc/wifi/PerProviderSubscription.conf).  In N and older, only Release 1 is supported.
     35  *
     36  * This class only retrieve the relevant Release 1 configuration fields that are not backed
     37  * elsewhere.  Below are relevant fields:
     38  * - FQDN (used for linking with configuration data stored elsewhere)
     39  * - Friendly Name
     40  * - Roaming Consortium
     41  * - Realm
     42  * - IMSI (for SIM credential)
     43  *
     44  * Below is an example content of a Passpoint configuration file:
     45  *
     46  * tree 3:1.2(urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0)
     47  * 8:MgmtTree+
     48  * 17:PerProviderSubscription+
     49  * 4:r1i1+
     50  * 6:HomeSP+
     51  * c:FriendlyName=d:Test Provider
     52  * 4:FQDN=8:test.net
     53  * 13:RoamingConsortiumOI=9:1234,5678
     54  * .
     55  * a:Credential+
     56  * 10:UsernamePassword+
     57  * 8:Username=4:user
     58  * 8:Password=4:pass
     59  *
     60  * 9:EAPMethod+
     61  * 7:EAPType=2:21
     62  * b:InnerMethod=3:PAP
     63  * .
     64  * .
     65  * 5:Realm=a:boingo.com
     66  * .
     67  * .
     68  * .
     69  * .
     70  *
     71  * Each string is prefixed with a "|StringBytesInHex|:".
     72  * '+' indicates start of a new internal node.
     73  * '.' indicates end of the current internal node.
     74  * '=' indicates "value" of a leaf node.
     75  *
     76  */
     77 public class LegacyPasspointConfigParser {
     78     private static final String TAG = "LegacyPasspointConfigParser";
     79 
     80     private static final String TAG_MANAGEMENT_TREE = "MgmtTree";
     81     private static final String TAG_PER_PROVIDER_SUBSCRIPTION = "PerProviderSubscription";
     82     private static final String TAG_HOMESP = "HomeSP";
     83     private static final String TAG_FQDN = "FQDN";
     84     private static final String TAG_FRIENDLY_NAME = "FriendlyName";
     85     private static final String TAG_ROAMING_CONSORTIUM_OI = "RoamingConsortiumOI";
     86     private static final String TAG_CREDENTIAL = "Credential";
     87     private static final String TAG_REALM = "Realm";
     88     private static final String TAG_SIM = "SIM";
     89     private static final String TAG_IMSI = "IMSI";
     90 
     91     private static final String LONG_ARRAY_SEPARATOR = ",";
     92     private static final String END_OF_INTERNAL_NODE_INDICATOR = ".";
     93     private static final char START_OF_INTERNAL_NODE_INDICATOR = '+';
     94     private static final char STRING_PREFIX_INDICATOR = ':';
     95     private static final char STRING_VALUE_INDICATOR = '=';
     96 
     97     /**
     98      * An abstraction for a node within a tree.  A node can be an internal node (contained
     99      * children nodes) or a leaf node (contained a String value).
    100      */
    101     private abstract static class Node {
    102         private final String mName;
    103         Node(String name) {
    104             mName = name;
    105         }
    106 
    107         /**
    108          * @return the name of the node
    109          */
    110         public String getName() {
    111             return mName;
    112         }
    113 
    114         /**
    115          * Applies for internal node only.
    116          *
    117          * @return the list of children nodes.
    118          */
    119         public abstract List<Node> getChildren();
    120 
    121         /**
    122          * Applies for leaf node only.
    123          *
    124          * @return the string value of the node
    125          */
    126         public abstract String getValue();
    127     }
    128 
    129     /**
    130      * Class representing an internal node of a tree.  It contained a list of child nodes.
    131      */
    132     private static class InternalNode extends Node {
    133         private final List<Node> mChildren;
    134         InternalNode(String name, List<Node> children) {
    135             super(name);
    136             mChildren = children;
    137         }
    138 
    139         @Override
    140         public List<Node> getChildren() {
    141             return mChildren;
    142         }
    143 
    144         @Override
    145         public String getValue() {
    146             return null;
    147         }
    148     }
    149 
    150     /**
    151      * Class representing a leaf node of a tree.  It contained a String type value.
    152      */
    153     private static class LeafNode extends Node {
    154         private final String mValue;
    155         LeafNode(String name, String value) {
    156             super(name);
    157             mValue = value;
    158         }
    159 
    160         @Override
    161         public List<Node> getChildren() {
    162             return null;
    163         }
    164 
    165         @Override
    166         public String getValue() {
    167             return mValue;
    168         }
    169     }
    170 
    171     public LegacyPasspointConfigParser() {}
    172 
    173     /**
    174      * Parse the legacy Passpoint configuration file content, only retrieve the relevant
    175      * configurations that are not saved elsewhere.
    176      *
    177      * For both N and M, only Release 1 is supported. Most of the configurations are saved
    178      * elsewhere as part of the {@link android.net.wifi.WifiConfiguration} data.
    179      * The configurations needed from the legacy Passpoint configuration file are:
    180      *
    181      * - FQDN - needed to be able to link to the associated {@link WifiConfiguration} data
    182      * - Friendly Name
    183      * - Roaming Consortium OIs
    184      * - Realm
    185      * - IMSI (for SIM credential)
    186      *
    187      * Make this function non-static so that it can be mocked during unit test.
    188      *
    189      * @param fileName The file name of the configuration file
    190      * @return Map of FQDN to {@link LegacyPasspointConfig}
    191      * @throws IOException
    192      */
    193     public Map<String, LegacyPasspointConfig> parseConfig(String fileName)
    194             throws IOException {
    195         Map<String, LegacyPasspointConfig> configs = new HashMap<>();
    196         BufferedReader in = new BufferedReader(new FileReader(fileName));
    197         in.readLine();      // Ignore the first line which contained the header.
    198 
    199         // Convert the configuration data to a management tree represented by a root {@link Node}.
    200         Node root = buildNode(in);
    201 
    202         if (root == null || root.getChildren() == null) {
    203             Log.d(TAG, "Empty configuration data");
    204             return configs;
    205         }
    206 
    207         // Verify root node name.
    208         if (!TextUtils.equals(TAG_MANAGEMENT_TREE, root.getName())) {
    209             throw new IOException("Unexpected root node: " + root.getName());
    210         }
    211 
    212         // Process and retrieve the configuration from each PPS (PerProviderSubscription) node.
    213         List<Node> ppsNodes = root.getChildren();
    214         for (Node ppsNode : ppsNodes) {
    215             LegacyPasspointConfig config = processPpsNode(ppsNode);
    216             configs.put(config.mFqdn, config);
    217         }
    218         return configs;
    219     }
    220 
    221     /**
    222      * Build a {@link Node} from the current line in the buffer.  A node can be an internal
    223      * node (ends with '+') or a leaf node.
    224      *
    225      * @param in Input buffer to read data from
    226      * @return {@link Node} representing the current line
    227      * @throws IOException
    228      */
    229     private static Node buildNode(BufferedReader in) throws IOException {
    230         // Read until non-empty line.
    231         String currentLine = null;
    232         while ((currentLine = in.readLine()) != null) {
    233             if (!currentLine.isEmpty()) {
    234                 break;
    235             }
    236         }
    237 
    238         // Return null if EOF is reached.
    239         if (currentLine == null) {
    240             return null;
    241         }
    242 
    243         // Remove the leading and the trailing whitespaces.
    244         currentLine = currentLine.trim();
    245 
    246         // Check for the internal node terminator.
    247         if (TextUtils.equals(END_OF_INTERNAL_NODE_INDICATOR, currentLine)) {
    248             return null;
    249         }
    250 
    251         // Parse the name-value of the current line.  The value will be null if the current line
    252         // is not a leaf node (e.g. line ends with a '+').
    253         // Each line is encoded in UTF-8.
    254         Pair<String, String> nameValuePair =
    255                 parseLine(currentLine.getBytes(StandardCharsets.UTF_8));
    256         if (nameValuePair.second != null) {
    257             return new LeafNode(nameValuePair.first, nameValuePair.second);
    258         }
    259 
    260         // Parse the children contained under this internal node.
    261         List<Node> children = new ArrayList<>();
    262         Node child = null;
    263         while ((child = buildNode(in)) != null) {
    264             children.add(child);
    265         }
    266         return new InternalNode(nameValuePair.first, children);
    267     }
    268 
    269     /**
    270      * Process a PPS (PerProviderSubscription) node to retrieve Passpoint configuration data.
    271      *
    272      * @param ppsNode The PPS node to process
    273      * @return {@link LegacyPasspointConfig}
    274      * @throws IOException
    275      */
    276     private static LegacyPasspointConfig processPpsNode(Node ppsNode) throws IOException {
    277         if (ppsNode.getChildren() == null || ppsNode.getChildren().size() != 1) {
    278             throw new IOException("PerProviderSubscription node should contain "
    279                     + "one instance node");
    280         }
    281 
    282         if (!TextUtils.equals(TAG_PER_PROVIDER_SUBSCRIPTION, ppsNode.getName())) {
    283             throw new IOException("Unexpected name for PPS node: " + ppsNode.getName());
    284         }
    285 
    286         // Retrieve the PPS instance node.
    287         Node instanceNode = ppsNode.getChildren().get(0);
    288         if (instanceNode.getChildren() == null) {
    289             throw new IOException("PPS instance node doesn't contained any children");
    290         }
    291 
    292         // Process and retrieve the relevant configurations under the PPS instance node.
    293         LegacyPasspointConfig config = new LegacyPasspointConfig();
    294         for (Node node : instanceNode.getChildren()) {
    295             switch (node.getName()) {
    296                 case TAG_HOMESP:
    297                     processHomeSPNode(node, config);
    298                     break;
    299                 case TAG_CREDENTIAL:
    300                     processCredentialNode(node, config);
    301                     break;
    302                 default:
    303                     Log.d(TAG, "Ignore uninterested field under PPS instance: " + node.getName());
    304                     break;
    305             }
    306         }
    307         if (config.mFqdn == null) {
    308             throw new IOException("PPS instance missing FQDN");
    309         }
    310         return config;
    311     }
    312 
    313     /**
    314      * Process a HomeSP node to retrieve configuration data into the given |config|.
    315      *
    316      * @param homeSpNode The HomeSP node to process
    317      * @param config The config object to fill in the data
    318      * @throws IOException
    319      */
    320     private static void processHomeSPNode(Node homeSpNode, LegacyPasspointConfig config)
    321             throws IOException {
    322         if (homeSpNode.getChildren() == null) {
    323             throw new IOException("HomeSP node should contain at least one child node");
    324         }
    325 
    326         for (Node node : homeSpNode.getChildren()) {
    327             switch (node.getName()) {
    328                 case TAG_FQDN:
    329                     config.mFqdn = getValue(node);
    330                     break;
    331                 case TAG_FRIENDLY_NAME:
    332                     config.mFriendlyName = getValue(node);
    333                     break;
    334                 case TAG_ROAMING_CONSORTIUM_OI:
    335                     config.mRoamingConsortiumOis = parseLongArray(getValue(node));
    336                     break;
    337                 default:
    338                     Log.d(TAG, "Ignore uninterested field under HomeSP: " + node.getName());
    339                     break;
    340             }
    341         }
    342     }
    343 
    344     /**
    345      * Process a Credential node to retrieve configuration data into the given |config|.
    346      *
    347      * @param credentialNode The Credential node to process
    348      * @param config The config object to fill in the data
    349      * @throws IOException
    350      */
    351     private static void processCredentialNode(Node credentialNode,
    352             LegacyPasspointConfig config)
    353             throws IOException {
    354         if (credentialNode.getChildren() == null) {
    355             throw new IOException("Credential node should contain at least one child node");
    356         }
    357 
    358         for (Node node : credentialNode.getChildren()) {
    359             switch (node.getName()) {
    360                 case TAG_REALM:
    361                     config.mRealm = getValue(node);
    362                     break;
    363                 case TAG_SIM:
    364                     processSimNode(node, config);
    365                     break;
    366                 default:
    367                     Log.d(TAG, "Ignore uninterested field under Credential: " + node.getName());
    368                     break;
    369             }
    370         }
    371     }
    372 
    373     /**
    374      * Process a SIM node to retrieve configuration data into the given |config|.
    375      *
    376      * @param simNode The SIM node to process
    377      * @param config The config object to fill in the data
    378      * @throws IOException
    379      */
    380     private static void processSimNode(Node simNode, LegacyPasspointConfig config)
    381             throws IOException {
    382         if (simNode.getChildren() == null) {
    383             throw new IOException("SIM node should contain at least one child node");
    384         }
    385 
    386         for (Node node : simNode.getChildren()) {
    387             switch (node.getName()) {
    388                 case TAG_IMSI:
    389                     config.mImsi = getValue(node);
    390                     break;
    391                 default:
    392                     Log.d(TAG, "Ignore uninterested field under SIM: " + node.getName());
    393                     break;
    394             }
    395         }
    396     }
    397 
    398     /**
    399      * Parse the given line in the legacy Passpoint configuration file.
    400      * A line can be in the following formats:
    401      * 2:ab+         // internal node
    402      * 2:ab=2:bc     // leaf node
    403      * .             // end of internal node
    404      *
    405      * @param line The line to parse
    406      * @return name-value pair, a value of null indicates internal node
    407      * @throws IOException
    408      */
    409     private static Pair<String, String> parseLine(byte[] lineBytes) throws IOException {
    410         Pair<String, Integer> nameIndexPair = parseString(lineBytes, 0);
    411         int currentIndex = nameIndexPair.second;
    412         try {
    413             if (lineBytes[currentIndex] == START_OF_INTERNAL_NODE_INDICATOR) {
    414                 return Pair.create(nameIndexPair.first, null);
    415             }
    416 
    417             if (lineBytes[currentIndex] != STRING_VALUE_INDICATOR) {
    418                 throw new IOException("Invalid line - missing both node and value indicator: "
    419                         + new String(lineBytes, StandardCharsets.UTF_8));
    420             }
    421         } catch (IndexOutOfBoundsException e) {
    422             throw new IOException("Invalid line - " + e.getMessage() + ": "
    423                     + new String(lineBytes, StandardCharsets.UTF_8));
    424         }
    425         Pair<String, Integer> valueIndexPair = parseString(lineBytes, currentIndex + 1);
    426         return Pair.create(nameIndexPair.first, valueIndexPair.first);
    427     }
    428 
    429     /**
    430      * Parse a string value in the given line from the given start index.
    431      * A string value is in the following format:
    432      * |HexByteLength|:|String|
    433      *
    434      * The length value indicates the number of UTF-8 bytes in hex for the given string.
    435      *
    436      * For example: 3:abc
    437      *
    438      * @param lineBytes The UTF-8 bytes of the line to parse
    439      * @param startIndex The start index from the given line to parse from
    440      * @return Pair of a string value and an index pointed to character after the string value
    441      * @throws IOException
    442      */
    443     private static Pair<String, Integer> parseString(byte[] lineBytes, int startIndex)
    444             throws IOException {
    445         // Locate the index that separate length and the string value.
    446         int prefixIndex = -1;
    447         for (int i = startIndex; i < lineBytes.length; i++) {
    448             if (lineBytes[i] == STRING_PREFIX_INDICATOR) {
    449                 prefixIndex = i;
    450                 break;
    451             }
    452         }
    453         if (prefixIndex == -1) {
    454             throw new IOException("Invalid line - missing string prefix: "
    455                     + new String(lineBytes, StandardCharsets.UTF_8));
    456         }
    457 
    458         try {
    459             String lengthStr = new String(lineBytes, startIndex, prefixIndex - startIndex,
    460                     StandardCharsets.UTF_8);
    461             int length = Integer.parseInt(lengthStr, 16);
    462             int strStartIndex = prefixIndex + 1;
    463             // The length might account for bytes for the whitespaces, since the whitespaces are
    464             // already trimmed, ignore them.
    465             if ((strStartIndex + length) > lineBytes.length) {
    466                 length = lineBytes.length - strStartIndex;
    467             }
    468             return Pair.create(
    469                     new String(lineBytes, strStartIndex, length, StandardCharsets.UTF_8),
    470                     strStartIndex + length);
    471         } catch (NumberFormatException | IndexOutOfBoundsException e) {
    472             throw new IOException("Invalid line - " + e.getMessage() + ": "
    473                     + new String(lineBytes, StandardCharsets.UTF_8));
    474         }
    475     }
    476 
    477     /**
    478      * Parse a long array from the given string.
    479      *
    480      * @param str The string to parse
    481      * @return long[]
    482      * @throws IOException
    483      */
    484     private static long[] parseLongArray(String str)
    485             throws IOException {
    486         String[] strArray = str.split(LONG_ARRAY_SEPARATOR);
    487         long[] longArray = new long[strArray.length];
    488         for (int i = 0; i < longArray.length; i++) {
    489             try {
    490                 longArray[i] = Long.parseLong(strArray[i], 16);
    491             } catch (NumberFormatException e) {
    492                 throw new IOException("Invalid long integer value: " + strArray[i]);
    493             }
    494         }
    495         return longArray;
    496     }
    497 
    498     /**
    499      * Get the String value of the given node.  An IOException will be thrown if the given
    500      * node doesn't contain a String value (internal node).
    501      *
    502      * @param node The node to get the value from
    503      * @return String
    504      * @throws IOException
    505      */
    506     private static String getValue(Node node) throws IOException {
    507         if (node.getValue() == null) {
    508             throw new IOException("Attempt to retreive value from non-leaf node: "
    509                     + node.getName());
    510         }
    511         return node.getValue();
    512     }
    513 }
    514