1 /*
2  * Copyright (C) 2021 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.imsserviceentitlement.utils;
18 
19 import static com.android.imsserviceentitlement.ts43.Ts43Constants.AccessTypeValue.LTE;
20 import static com.android.imsserviceentitlement.ts43.Ts43Constants.AccessTypeValue.NGRAN;
21 import static com.android.imsserviceentitlement.ts43.Ts43Constants.HomeRoamingNwTypeValue.ALL;
22 import static com.android.imsserviceentitlement.ts43.Ts43Constants.HomeRoamingNwTypeValue.HOME;
23 import static com.android.imsserviceentitlement.ts43.Ts43Constants.HomeRoamingNwTypeValue.ROAMING;
24 import static com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlAttributes.ACCESS_TYPE;
25 import static com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlAttributes.APP_ID;
26 import static com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlAttributes.HOME_ROAMING_NW_TYPE;
27 import static com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlAttributes.RAT_VOICE_ENTITLE_INFO_DETAILS;
28 import static com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlNode.APPLICATION;
29 import static com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlNode.TOKEN;
30 import static com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlNode.VERS;
31 
32 import android.text.TextUtils;
33 import android.util.ArrayMap;
34 import android.util.Log;
35 
36 import com.android.imsserviceentitlement.debug.DebugUtils;
37 import com.android.libraries.entitlement.ServiceEntitlement;
38 
39 import org.w3c.dom.Document;
40 import org.w3c.dom.Element;
41 import org.w3c.dom.NamedNodeMap;
42 import org.w3c.dom.Node;
43 import org.w3c.dom.NodeList;
44 import org.xml.sax.InputSource;
45 import org.xml.sax.SAXException;
46 
47 import java.io.IOException;
48 import java.io.StringReader;
49 import java.util.Map;
50 import java.util.Optional;
51 
52 import javax.xml.parsers.DocumentBuilder;
53 import javax.xml.parsers.DocumentBuilderFactory;
54 import javax.xml.parsers.ParserConfigurationException;
55 
56 /** Wrap the raw content and parse it into nodes. */
57 public class XmlDoc {
58     private static final String TAG = "IMSSE-XmlDoc";
59 
60     private static final String NODE_CHARACTERISTIC = "characteristic";
61     private static final String NODE_PARM = "parm";
62     private static final String PARM_NAME = "name";
63     private static final String PARM_VALUE = "value";
64 
65     private final Map<String, Map<String, String>> mNodesMap = new ArrayMap<>();
66 
XmlDoc(String responseBody)67     public XmlDoc(String responseBody) {
68         parseXmlResponse(responseBody);
69     }
70 
getFromToken(String key)71     public Optional<String> getFromToken(String key) {
72         Map<String, String> paramsMap = mNodesMap.get(TOKEN);
73         return Optional.ofNullable(paramsMap == null ? null : paramsMap.get(key));
74     }
75 
getFromVersion(String key)76     public Optional<String> getFromVersion(String key) {
77         Map<String, String> paramsMap = mNodesMap.get(VERS);
78         return Optional.ofNullable(paramsMap == null ? null : paramsMap.get(key));
79     }
80 
getFromVowifi(String key)81     public Optional<String> getFromVowifi(String key) {
82         Map<String, String> paramsMap = mNodesMap.get(ServiceEntitlement.APP_VOWIFI);
83         return Optional.ofNullable(paramsMap == null ? null : paramsMap.get(key));
84     }
85 
getFromVolte(String key)86     public Optional<String> getFromVolte(String key) {
87         Map<String, String> paramsMap = mNodesMap.get(ServiceEntitlement.APP_VOLTE);
88         paramsMap = paramsMap == null ? mNodesMap.get(LTE + ALL) : paramsMap;
89         paramsMap = paramsMap == null ? mNodesMap.get(LTE + HOME) : paramsMap;
90         return Optional.ofNullable(paramsMap == null ? null : paramsMap.get(key));
91     }
92 
getFromSmsoverip(String key)93     public Optional<String> getFromSmsoverip(String key) {
94         Map<String, String> paramsMap = mNodesMap.get(ServiceEntitlement.APP_SMSOIP);
95         return Optional.ofNullable(paramsMap == null ? null : paramsMap.get(key));
96     }
97 
getFromVonrHome(String key)98     public Optional<String> getFromVonrHome(String key) {
99         Map<String, String> paramsMap = mNodesMap.get(NGRAN + ALL);
100         paramsMap = paramsMap == null ? mNodesMap.get(NGRAN + HOME) : paramsMap;
101         return Optional.ofNullable(paramsMap == null ? null : paramsMap.get(key));
102     }
103 
getFromVonrRoaming(String key)104     public Optional<String> getFromVonrRoaming(String key) {
105         Map<String, String> paramsMap = mNodesMap.get(NGRAN + ALL);
106         paramsMap = paramsMap == null ? mNodesMap.get(NGRAN + ROAMING) : paramsMap;
107         return Optional.ofNullable(paramsMap == null ? null : paramsMap.get(key));
108     }
109 
110     /**
111      * Parses the response body as per format defined in TS.43 2.7.2 New Characteristics for
112      * XML-Based Document.
113      */
parseXmlResponse(String responseBody)114     private void parseXmlResponse(String responseBody) {
115         if (responseBody == null) {
116             return;
117         }
118 
119         if (DebugUtils.isPiiLoggable()) {
120             Log.d(TAG, "Raw Response Body: " + responseBody);
121         }
122         // Workaround: some server doesn't escape "&" in XML response and that will cause XML parser
123         // failure later.
124         // This is a quick impl of escaping w/o intorducing a ton of new dependencies.
125         responseBody = responseBody.replace("&", "&amp;").replace("&amp;amp;", "&amp;");
126 
127         try {
128             InputSource inputSource = new InputSource(new StringReader(responseBody));
129             DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
130             DocumentBuilder docBuilder = builderFactory.newDocumentBuilder();
131             Document doc = docBuilder.parse(inputSource);
132             doc.getDocumentElement().normalize();
133 
134             if (DebugUtils.isPiiLoggable()) {
135                 Log.d(
136                         TAG,
137                         "parseXmlResponseForNode() Root element: "
138                                 + doc.getDocumentElement().getNodeName());
139             }
140 
141             NodeList nodeList = doc.getElementsByTagName(NODE_CHARACTERISTIC);
142             for (int i = 0; i < nodeList.getLength(); i++) {
143                 NamedNodeMap map = nodeList.item(i).getAttributes();
144                 if (DebugUtils.isPiiLoggable()) {
145                     Log.d(
146                             TAG,
147                             "parseAuthenticateResponse() node name="
148                                     + nodeList.item(i).getNodeName()
149                                     + " node value="
150                                     + map.item(0).getNodeValue());
151                 }
152                 Element element = (Element) nodeList.item(i);
153                 if (element.getElementsByTagName(NODE_CHARACTERISTIC).getLength() != 0) {
154                     continue;
155                 }
156 
157                 Map<String, String> paramsMap = new ArrayMap<>();
158                 String characteristicType = map.item(0).getNodeValue();
159                 String key;
160                 paramsMap.putAll(parseParams(element.getElementsByTagName(NODE_PARM)));
161                 if (APPLICATION.equals(characteristicType)) {
162                     key = paramsMap.get(APP_ID);
163                 } else if (RAT_VOICE_ENTITLE_INFO_DETAILS.equals(characteristicType)) {
164                     key = paramsMap.get(ACCESS_TYPE) + paramsMap.get(HOME_ROAMING_NW_TYPE);
165                 } else { // VERS or TOKEN
166                     key = characteristicType;
167                 }
168                 mNodesMap.put(key, paramsMap);
169             }
170         } catch (ParserConfigurationException | IOException | SAXException e) {
171             Log.e(TAG, "Failed to parse XML node. " + e);
172         }
173     }
174 
parseParams(NodeList nodeList)175     private static Map<String, String> parseParams(NodeList nodeList) {
176         Map<String, String> nameValue = new ArrayMap<>();
177         for (int i = 0; i < nodeList.getLength(); i++) {
178             Node node = nodeList.item(i);
179             NamedNodeMap map = node.getAttributes();
180             String name = "";
181             String value = "";
182             for (int j = 0; j < map.getLength(); j++) {
183                 if (PARM_NAME.equals(map.item(j).getNodeName())) {
184                     name = map.item(j).getNodeValue();
185                 } else if (PARM_VALUE.equals(map.item(j).getNodeName())) {
186                     value = map.item(j).getNodeValue();
187                 }
188             }
189             if (TextUtils.isEmpty(name) || TextUtils.isEmpty(value)) {
190                 continue;
191             }
192             nameValue.put(name, value);
193 
194             if (DebugUtils.isPiiLoggable()) {
195                 Log.d(TAG, "parseParams() put name '" + name + "' with value " + value);
196             }
197         }
198         return nameValue;
199     }
200 }
201