1 /*
2  * Copyright (C) 2020 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.internal.telephony;
18 
19 import android.net.Uri;
20 import android.util.ArraySet;
21 import android.util.Log;
22 import android.util.Pair;
23 
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collections;
27 import java.util.List;
28 import java.util.Set;
29 import java.util.stream.Collectors;
30 
31 /**
32  * Utility methods for parsing parts of {@link android.telephony.ims.SipMessage}s.
33  * See RFC 3261 for more information.
34  * @hide
35  */
36 // Note: This is lightweight in order to avoid a full SIP stack import in frameworks/base.
37 public class SipMessageParsingUtils {
38     private static final String TAG = "SipMessageParsingUtils";
39     // "Method" in request-line
40     // Request-Line = Method SP Request-URI SP SIP-Version CRLF
41     private static final String[] SIP_REQUEST_METHODS = new String[] {"INVITE", "ACK", "OPTIONS",
42             "BYE", "CANCEL", "REGISTER", "PRACK", "SUBSCRIBE", "NOTIFY", "PUBLISH", "INFO", "REFER",
43             "MESSAGE", "UPDATE"};
44 
45     // SIP Version 2.0 (corresponding to RCS 3261), set in "SIP-Version" of Status-Line and
46     // Request-Line
47     //
48     // Request-Line = Method SP Request-URI SP SIP-Version CRLF
49     // Status-Line = SIP-Version SP Status-Code SP Reason-Phrase CRLF
50     private static final String SIP_VERSION_2 = "SIP/2.0";
51 
52     // headers are formatted Key:Value
53     private static final String HEADER_KEY_VALUE_SEPARATOR = ":";
54     // Multiple of the same header can be concatenated and put into one header Key:Value pair, for
55     // example "v: XX1;branch=YY1,XX2;branch=YY2". This needs to be treated as two "v:" headers.
56     private static final String SUBHEADER_VALUE_SEPARATOR = ",";
57 
58     // SIP header parameters have the format ";paramName=paramValue"
59     private static final String PARAM_SEPARATOR = ";";
60     // parameters are formatted paramName=ParamValue
61     private static final String PARAM_KEY_VALUE_SEPARATOR = "=";
62 
63     // The via branch parameter definition
64     private static final String BRANCH_PARAM_KEY = "branch";
65 
66     // via header key
67     private static final String VIA_SIP_HEADER_KEY = "via";
68     // compact form of the via header key
69     private static final String VIA_SIP_HEADER_KEY_COMPACT = "v";
70 
71     // call-id header key
72     private static final String CALL_ID_SIP_HEADER_KEY = "call-id";
73     // compact form of the call-id header key
74     private static final String CALL_ID_SIP_HEADER_KEY_COMPACT = "i";
75 
76     // from header key
77     private static final String FROM_HEADER_KEY = "from";
78     // compact form of the from header key
79     private static final String FROM_HEADER_KEY_COMPACT = "f";
80 
81     // to header key
82     private static final String TO_HEADER_KEY = "to";
83     // compact form of the to header key
84     private static final String TO_HEADER_KEY_COMPACT = "t";
85 
86     // The tag parameter found in both the from and to headers
87     private static final String TAG_PARAM_KEY = "tag";
88 
89     // accept-contact header key
90     private static final String ACCEPT_CONTACT_HEADER_KEY = "accept-contact";
91     // compact form of the accept-contact header key
92     private static final String ACCEPT_CONTACT_HEADER_KEY_COMPACT = "a";
93 
94     /**
95      * @return true if the SIP message start line is considered a request (based on known request
96      * methods).
97      */
isSipRequest(String startLine)98     public static boolean isSipRequest(String startLine) {
99         String[] splitLine = splitStartLineAndVerify(startLine);
100         if (splitLine == null) return false;
101         return verifySipRequest(splitLine);
102     }
103 
104     /**
105      * @return true if the SIP message start line is considered a response.
106      */
isSipResponse(String startLine)107     public static boolean isSipResponse(String startLine) {
108         String[] splitLine = splitStartLineAndVerify(startLine);
109         if (splitLine == null) return false;
110         return verifySipResponse(splitLine);
111     }
112 
113     /**
114      * Return the via branch parameter, which is used to identify the transaction ID (request and
115      * response pair) in a SIP transaction.
116      * @param headerString The string containing the headers of the SIP message.
117      */
getTransactionId(String headerString)118     public static String getTransactionId(String headerString) {
119         // search for Via: or v: parameter, we only care about the first one.
120         List<Pair<String, String>> headers = parseHeaders(headerString, true,
121                 VIA_SIP_HEADER_KEY, VIA_SIP_HEADER_KEY_COMPACT);
122         for (Pair<String, String> header : headers) {
123             // Headers can also be concatenated together using a "," between each header value.
124             // format becomes v: XX1;branch=YY1,XX2;branch=YY2. Need to extract only the first ID's
125             // branch param YY1.
126             String[] subHeaders = header.second.split(SUBHEADER_VALUE_SEPARATOR);
127             for (String subHeader : subHeaders) {
128                 String paramValue = getParameterValue(subHeader, BRANCH_PARAM_KEY);
129                 if (paramValue == null) continue;
130                 return paramValue;
131             }
132         }
133         return null;
134     }
135 
136     /**
137      * Search a header's value for a specific parameter.
138      * @param headerValue The header key's value.
139      * @param parameterKey The parameter key we are looking for.
140      * @return The value associated with the specified parameter key or {@link null} if that key is
141      * not found.
142      */
getParameterValue(String headerValue, String parameterKey)143     private static String getParameterValue(String headerValue, String parameterKey) {
144         String[] params = headerValue.split(PARAM_SEPARATOR);
145         if (params.length < 2) {
146             return null;
147         }
148         // by spec, each param can only appear once in a header.
149         for (String param : params) {
150             String[] pair = param.split(PARAM_KEY_VALUE_SEPARATOR);
151             if (pair.length < 2) {
152                 // ignore info before the first parameter
153                 continue;
154             }
155             if (pair.length > 2) {
156                 Log.w(TAG,
157                         "getParameterValue: unexpected parameter" + Arrays.toString(pair));
158             }
159             // Trim whitespace in parameter
160             pair[0] = pair[0].trim();
161             pair[1] = pair[1].trim();
162             if (parameterKey.equalsIgnoreCase(pair[0])) {
163                 return pair[1];
164             }
165         }
166         return null;
167     }
168 
169     /**
170      * Return the call-id header key's associated value.
171      * @param headerString The string containing the headers of the SIP message.
172      */
getCallId(String headerString)173     public static String getCallId(String headerString) {
174         // search for the call-Id header, there should only be one in the headers.
175         List<Pair<String, String>> headers = parseHeaders(headerString, true,
176                 CALL_ID_SIP_HEADER_KEY, CALL_ID_SIP_HEADER_KEY_COMPACT);
177         return !headers.isEmpty() ? headers.get(0).second : null;
178     }
179 
180     /**
181      * @return Return the from header's tag parameter or {@code null} if it doesn't exist.
182      */
getFromTag(String headerString)183     public static String getFromTag(String headerString) {
184         // search for the from header, there should only be one in the headers.
185         List<Pair<String, String>> headers = parseHeaders(headerString, true,
186                 FROM_HEADER_KEY, FROM_HEADER_KEY_COMPACT);
187         if (headers.isEmpty()) {
188             return null;
189         }
190         // There should only be one from header in the SIP message
191         return getParameterValue(headers.get(0).second, TAG_PARAM_KEY);
192     }
193 
194     /**
195      * @return Return the to header's tag parameter or {@code null} if it doesn't exist.
196      */
getToTag(String headerString)197     public static String getToTag(String headerString) {
198         // search for the to header, there should only be one in the headers.
199         List<Pair<String, String>> headers = parseHeaders(headerString, true,
200                 TO_HEADER_KEY, TO_HEADER_KEY_COMPACT);
201         if (headers.isEmpty()) {
202             return null;
203         }
204         // There should only be one from header in the SIP message
205         return getParameterValue(headers.get(0).second, TAG_PARAM_KEY);
206     }
207 
208     /**
209      * Validate that the start line is correct and split into its three segments.
210      * @param startLine The start line to verify and split.
211      * @return The split start line, which will always have three segments.
212      */
splitStartLineAndVerify(String startLine)213     public static String[] splitStartLineAndVerify(String startLine) {
214         String[] splitLine = startLine.split(" ", 3);
215         if (isStartLineMalformed(splitLine)) return null;
216         return splitLine;
217     }
218 
219 
220     /**
221      * @return All feature tags starting with "+" in the Accept-Contact header.
222      */
getAcceptContactFeatureTags(String headerString)223     public static Set<String> getAcceptContactFeatureTags(String headerString) {
224         List<Pair<String, String>> headers = SipMessageParsingUtils.parseHeaders(headerString,
225                 false, ACCEPT_CONTACT_HEADER_KEY, ACCEPT_CONTACT_HEADER_KEY_COMPACT);
226         if (headerString.isEmpty()) {
227             return Collections.emptySet();
228         }
229         Set<String> featureTags = new ArraySet<>();
230         for (Pair<String, String> header : headers) {
231             String[] splitParams = header.second.split(PARAM_SEPARATOR);
232             if (splitParams.length < 2) {
233                 continue;
234             }
235             // Start at 1 here, since the first entry is the header value and not params.
236             // We only care about IMS feature tags here, so filter tags with a "+"
237             Set<String> fts = Arrays.asList(splitParams).subList(1, splitParams.length).stream()
238                     .map(String::trim).filter(p -> p.startsWith("+")).collect(Collectors.toSet());
239             for (String ft : fts) {
240                 String[] paramKeyValue = ft.split(PARAM_KEY_VALUE_SEPARATOR, 2);
241                 if (paramKeyValue.length < 2) {
242                     featureTags.add(ft);
243                     continue;
244                 }
245                 // Splits keys like +a="b,c" into +a="b" and +a="c"
246                 String[] splitValue = splitParamValue(paramKeyValue[1]);
247                 for (String value : splitValue) {
248                     featureTags.add(paramKeyValue[0] + PARAM_KEY_VALUE_SEPARATOR + value);
249                 }
250             }
251         }
252         return featureTags;
253     }
254 
255     /**
256      * Takes a string such as "\"a,b,c,d\"" and splits it by "," into a String array of
257      * [\"a\", \"b\", \"c\", \"d\"]
258      */
splitParamValue(String paramValue)259     private static String[] splitParamValue(String paramValue) {
260         if (!paramValue.startsWith("\"") && !paramValue.endsWith("\"")) {
261             return new String[] {paramValue};
262         }
263         // Remove quotes on outside
264         paramValue = paramValue.substring(1, paramValue.length() - 1);
265         String[] splitValues = paramValue.split(",");
266         for (int i = 0; i < splitValues.length; i++) {
267             // Encapsulate each split value in its own quotations.
268             splitValues[i] = "\"" + splitValues[i] + "\"";
269         }
270         return splitValues;
271     }
272 
isStartLineMalformed(String[] startLine)273     private static boolean isStartLineMalformed(String[] startLine) {
274         if (startLine == null || startLine.length == 0)  {
275             return true;
276         }
277         // start lines contain three segments separated by spaces (SP):
278         // Request-Line  =  Method SP Request-URI SP SIP-Version CRLF
279         // Status-Line  =  SIP-Version SP Status-Code SP Reason-Phrase CRLF
280         return (startLine.length != 3);
281     }
282 
verifySipRequest(String[] request)283     private static boolean verifySipRequest(String[] request) {
284         // Request-Line  =  Method SP Request-URI SP SIP-Version CRLF
285         if (!request[2].contains(SIP_VERSION_2)) return false;
286         boolean verified;
287         try {
288             verified = (Uri.parse(request[1]).getScheme() != null);
289         } catch (NumberFormatException e) {
290             return false;
291         }
292         verified &= Arrays.stream(SIP_REQUEST_METHODS).anyMatch(s -> request[0].contains(s));
293         return verified;
294     }
295 
verifySipResponse(String[] response)296     private static boolean verifySipResponse(String[] response) {
297         // Status-Line = SIP-Version SP Status-Code SP Reason-Phrase CRLF
298         if (!response[0].contains(SIP_VERSION_2)) return false;
299         int statusCode;
300         try {
301             statusCode = Integer.parseInt(response[1]);
302         } catch (NumberFormatException e) {
303             return false;
304         }
305         return (statusCode >= 100  && statusCode < 700);
306     }
307 
308     /**
309      * Parse a String representation of the Header portion of the SIP Message and re-structure it
310      * into a List of key->value pairs representing each header in the order that they appeared in
311      * the message.
312      *
313      * @param headerString The raw string containing all headers
314      * @param stopAtFirstMatch Return early when the first match is found from matching header keys.
315      * @param matchingHeaderKeys An optional list of Strings containing header keys that should be
316      *                           returned if they exist. If none exist, all keys will be returned.
317      *                           (This is internally an equalsIgnoreMatch comparison).
318      * @return the matched header keys and values.
319      */
parseHeaders(String headerString, boolean stopAtFirstMatch, String... matchingHeaderKeys)320     public static List<Pair<String, String>> parseHeaders(String headerString,
321             boolean stopAtFirstMatch, String... matchingHeaderKeys) {
322         // Ensure there is no leading whitespace
323         headerString = removeLeadingWhitespace(headerString);
324 
325         List<Pair<String, String>> result = new ArrayList<>();
326         // Split the string line-by-line.
327         String[] headerLines = headerString.split("\\r?\\n");
328         if (headerLines.length == 0) {
329             return Collections.emptyList();
330         }
331 
332         String headerKey = null;
333         StringBuilder headerValueSegment = new StringBuilder();
334         // loop through each line, either parsing a "key: value" pair or appending values that span
335         // multiple lines.
336         for (String line : headerLines) {
337             // This line is a continuation of the last line if it starts with whitespace or tab
338             if (line.startsWith("\t") || line.startsWith(" ")) {
339                 headerValueSegment.append(removeLeadingWhitespace(line));
340                 continue;
341             }
342             // This line is the start of a new key, If headerKey/value is already populated from a
343             // previous key/value pair, add it to list of parsed header pairs.
344             if (headerKey != null) {
345                 final String key = headerKey;
346                 if (matchingHeaderKeys == null || matchingHeaderKeys.length == 0
347                         || Arrays.stream(matchingHeaderKeys).anyMatch(
348                                 (s) -> s.equalsIgnoreCase(key))) {
349                     result.add(new Pair<>(key, headerValueSegment.toString()));
350                     if (stopAtFirstMatch) {
351                         return result;
352                     }
353                 }
354                 headerKey = null;
355                 headerValueSegment = new StringBuilder();
356             }
357 
358             // Format is "Key:Value", ignore any ":" after the first.
359             String[] pair = line.split(HEADER_KEY_VALUE_SEPARATOR, 2);
360             if (pair.length < 2) {
361                 // malformed line, skip
362                 Log.w(TAG, "parseHeaders - received malformed line: " + line);
363                 continue;
364             }
365 
366             headerKey = pair[0].trim();
367             for (int i = 1; i < pair.length; i++) {
368                 headerValueSegment.append(removeLeadingWhitespace(pair[i]));
369             }
370         }
371         // Pick up the last pending header being parsed, if it exists.
372         if (headerKey != null) {
373             final String key = headerKey;
374             if (matchingHeaderKeys == null || matchingHeaderKeys.length == 0
375                     || Arrays.stream(matchingHeaderKeys).anyMatch(
376                             (s) -> s.equalsIgnoreCase(key))) {
377                 result.add(new Pair<>(key, headerValueSegment.toString()));
378             }
379         }
380 
381         return result;
382     }
383 
removeLeadingWhitespace(String line)384     private static String removeLeadingWhitespace(String line) {
385         return line.replaceFirst("^\\s*", "");
386     }
387 }
388