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