Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ee022003 authored by Brad Ebinger's avatar Brad Ebinger Committed by Gerrit Code Review
Browse files

Merge "Parse SIP Messages for Transaction ID in SIP transport"

parents f1273df6 5a9d8c49
Loading
Loading
Loading
Loading
+238 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.telephony;

import android.net.Uri;
import android.util.Log;
import android.util.Pair;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * Utility methods for parsing parts of {@link android.telephony.ims.SipMessage}s.
 * See RFC 3261 for more information.
 * @hide
 */
// Note: This is lightweight in order to avoid a full SIP stack import in frameworks/base.
public class SipMessageParsingUtils {
    private static final String TAG = "SipMessageParsingUtils";
    // "Method" in request-line
    // Request-Line = Method SP Request-URI SP SIP-Version CRLF
    private static final String[] SIP_REQUEST_METHODS = new String[] {"INVITE", "ACK", "OPTIONS",
            "BYE", "CANCEL", "REGISTER", "PRACK", "SUBSCRIBE", "NOTIFY", "PUBLISH", "INFO", "REFER",
            "MESSAGE", "UPDATE"};

    // SIP Version 2.0 (corresponding to RCS 3261), set in "SIP-Version" of Status-Line and
    // Request-Line
    //
    // Request-Line = Method SP Request-URI SP SIP-Version CRLF
    // Status-Line = SIP-Version SP Status-Code SP Reason-Phrase CRLF
    private static final String SIP_VERSION_2 = "SIP/2.0";

    // headers are formatted Key:Value
    private static final String HEADER_KEY_VALUE_SEPARATOR = ":";
    // Multiple of the same header can be concatenated and put into one header Key:Value pair, for
    // example "v: XX1;branch=YY1,XX2;branch=YY2". This needs to be treated as two "v:" headers.
    private static final String SUBHEADER_VALUE_SEPARATOR = ",";

    // SIP header parameters have the format ";paramName=paramValue"
    private static final String PARAM_SEPARATOR = ";";
    // parameters are formatted paramName=ParamValue
    private static final String PARAM_KEY_VALUE_SEPARATOR = "=";

    // The via branch parameter definition
    private static final String BRANCH_PARAM_KEY = "branch";

    // via header key
    private static final String VIA_SIP_HEADER_KEY = "via";
    // compact form of the via header key
    private static final String VIA_SIP_HEADER_KEY_COMPACT = "v";

    /**
     * @return true if the SIP message start line is considered a request (based on known request
     * methods).
     */
    public static boolean isSipRequest(String startLine) {
        String[] splitLine = splitStartLineAndVerify(startLine);
        if (splitLine == null) return false;
        return verifySipRequest(splitLine);
    }

    /**
     * Return the via branch parameter, which is used to identify the transaction ID (request and
     * response pair) in a SIP transaction.
     * @param headerString The string containing the headers of the SIP message.
     */
    public static String getTransactionId(String headerString) {
        // search for Via: or v: parameter, we only care about the first one.
        List<Pair<String, String>> headers = parseHeaders(headerString, true,
                VIA_SIP_HEADER_KEY, VIA_SIP_HEADER_KEY_COMPACT);
        for (Pair<String, String> header : headers) {
            // Headers can also be concatenated together using a "," between each header value.
            // format becomes v: XX1;branch=YY1,XX2;branch=YY2. Need to extract only the first ID's
            // branch param YY1.
            String[] subHeaders = header.second.split(SUBHEADER_VALUE_SEPARATOR);
            for (String subHeader : subHeaders) {
                // Search for ;branch=z9hG4bKXXXXXX and return parameter value
                String[] params = subHeader.split(PARAM_SEPARATOR);
                if (params.length < 2) {
                    // This param doesn't include a branch param, move to next param.
                    Log.w(TAG, "getTransactionId: via detected without branch param:"
                            + subHeader);
                    continue;
                }
                // by spec, each param can only appear once in a header.
                for (String param : params) {
                    String[] pair = param.split(PARAM_KEY_VALUE_SEPARATOR);
                    if (pair.length < 2) {
                        // ignore info before the first parameter
                        continue;
                    }
                    if (pair.length > 2) {
                        Log.w(TAG,
                                "getTransactionId: unexpected parameter" + Arrays.toString(pair));
                    }
                    // Trim whitespace in parameter
                    pair[0] = pair[0].trim();
                    pair[1] = pair[1].trim();
                    if (BRANCH_PARAM_KEY.equalsIgnoreCase(pair[0])) {
                        // There can be multiple "Via" headers in the SIP message, however we want
                        // to return the first once found, as this corresponds with the transaction
                        // that is relevant here.
                        return pair[1];
                    }
                }
            }
        }
        return null;
    }

    private static String[] splitStartLineAndVerify(String startLine) {
        String[] splitLine = startLine.split(" ");
        if (isStartLineMalformed(splitLine)) return null;
        return splitLine;
    }

    private static boolean isStartLineMalformed(String[] startLine) {
        if (startLine == null || startLine.length == 0)  {
            return true;
        }
        // start lines contain three segments separated by spaces (SP):
        // Request-Line  =  Method SP Request-URI SP SIP-Version CRLF
        // Status-Line  =  SIP-Version SP Status-Code SP Reason-Phrase CRLF
        return (startLine.length != 3);
    }

    private static boolean verifySipRequest(String[] request) {
        // Request-Line  =  Method SP Request-URI SP SIP-Version CRLF
        boolean verified = request[2].contains(SIP_VERSION_2);
        verified &= (Uri.parse(request[1]).getScheme() != null);
        verified &= Arrays.stream(SIP_REQUEST_METHODS).anyMatch(s -> request[0].contains(s));
        return verified;
    }

    private static boolean verifySipResponse(String[] response) {
        // Status-Line = SIP-Version SP Status-Code SP Reason-Phrase CRLF
        boolean verified = response[0].contains(SIP_VERSION_2);
        int statusCode = Integer.parseInt(response[1]);
        verified &= (statusCode >= 100  && statusCode < 700);
        return verified;
    }

    /**
     * Parse a String representation of the Header portion of the SIP Message and re-structure it
     * into a List of key->value pairs representing each header in the order that they appeared in
     * the message.
     *
     * @param headerString The raw string containing all headers
     * @param stopAtFirstMatch Return early when the first match is found from matching header keys.
     * @param matchingHeaderKeys An optional list of Strings containing header keys that should be
     *                           returned if they exist. If none exist, all keys will be returned.
     *                           (This is internally an equalsIgnoreMatch comparison).
     * @return the matched header keys and values.
     */
    private static List<Pair<String, String>> parseHeaders(String headerString,
            boolean stopAtFirstMatch, String... matchingHeaderKeys) {
        // Ensure there is no leading whitespace
        headerString = removeLeadingWhitespace(headerString);

        List<Pair<String, String>> result = new ArrayList<>();
        // Split the string line-by-line.
        String[] headerLines = headerString.split("\\r?\\n");
        if (headerLines.length == 0) {
            return Collections.emptyList();
        }

        String headerKey = null;
        StringBuilder headerValueSegment = new StringBuilder();
        // loop through each line, either parsing a "key: value" pair or appending values that span
        // multiple lines.
        for (String line : headerLines) {
            // This line is a continuation of the last line if it starts with whitespace or tab
            if (line.startsWith("\t") || line.startsWith(" ")) {
                headerValueSegment.append(removeLeadingWhitespace(line));
                continue;
            }
            // This line is the start of a new key, If headerKey/value is already populated from a
            // previous key/value pair, add it to list of parsed header pairs.
            if (headerKey != null) {
                final String key = headerKey;
                if (matchingHeaderKeys == null || matchingHeaderKeys.length == 0
                        || Arrays.stream(matchingHeaderKeys).anyMatch(
                                (s) -> s.equalsIgnoreCase(key))) {
                    result.add(new Pair<>(key, headerValueSegment.toString()));
                    if (stopAtFirstMatch) {
                        return result;
                    }
                }
                headerKey = null;
                headerValueSegment = new StringBuilder();
            }

            // Format is "Key:Value", ignore any ":" after the first.
            String[] pair = line.split(HEADER_KEY_VALUE_SEPARATOR, 2);
            if (pair.length < 2) {
                // malformed line, skip
                Log.w(TAG, "parseHeaders - received malformed line: " + line);
                continue;
            }

            headerKey = pair[0].trim();
            for (int i = 1; i < pair.length; i++) {
                headerValueSegment.append(removeLeadingWhitespace(pair[i]));
            }
        }
        // Pick up the last pending header being parsed, if it exists.
        if (headerKey != null) {
            final String key = headerKey;
            if (matchingHeaderKeys == null || matchingHeaderKeys.length == 0
                    || Arrays.stream(matchingHeaderKeys).anyMatch(
                            (s) -> s.equalsIgnoreCase(key))) {
                result.add(new Pair<>(key, headerValueSegment.toString()));
            }
        }

        return result;
    }

    private static String removeLeadingWhitespace(String line) {
        return line.replaceFirst("^\\s*", "");
    }
}
+8 −17
Original line number Diff line number Diff line
@@ -22,6 +22,8 @@ import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;

import com.android.internal.telephony.SipMessageParsingUtils;

import java.util.Arrays;
import java.util.Objects;

@@ -38,9 +40,6 @@ public final class SipMessage implements Parcelable {
    // Should not be set to true for production!
    private static final boolean IS_DEBUGGING = Build.IS_ENG;

    private static final String[] SIP_REQUEST_METHODS = new String[] {"INVITE", "ACK", "OPTIONS",
            "BYE", "CANCEL", "REGISTER"};

    private final String mStartLine;
    private final String mHeaderSection;
    private final byte[] mContent;
@@ -72,6 +71,7 @@ public final class SipMessage implements Parcelable {
        mContent = new byte[source.readInt()];
        source.readByteArray(mContent);
    }

    /**
     * @return The start line of the SIP message, which contains either the request-line or
     * status-line.
@@ -128,35 +128,26 @@ public final class SipMessage implements Parcelable {
        } else {
            b.append(sanitizeStartLineRequest(mStartLine));
        }
        b.append("], [");
        b.append("Header: [");
        b.append("], Header: [");
        if (IS_DEBUGGING) {
            b.append(mHeaderSection);
        } else {
            // only identify transaction id/call ID when it is available.
            b.append("***");
        }
        b.append("], ");
        b.append("Content: [NOT SHOWN]");
        b.append("], Content: ");
        b.append(getContent().length == 0 ? "[NONE]" : "[NOT SHOWN]");
        return b.toString();
    }

    /**
     * Start lines containing requests are formatted: METHOD SP Request-URI SP SIP-Version CRLF.
     * Detect if this is a REQUEST and redact Request-URI portion here, as it contains PII.
     */
    private String sanitizeStartLineRequest(String startLine) {
        if (!SipMessageParsingUtils.isSipRequest(startLine)) return startLine;
        String[] splitLine = startLine.split(" ");
        if (splitLine == null || splitLine.length == 0)  {
            return "(INVALID STARTLINE)";
        }
        for (String method : SIP_REQUEST_METHODS) {
            if (splitLine[0].contains(method)) {
        return splitLine[0] + " <Request-URI> " + splitLine[2];
    }
        }
        return startLine;
    }

    @Override
    public boolean equals(Object o) {
+12 −3
Original line number Diff line number Diff line
@@ -28,6 +28,10 @@ import android.telephony.ims.SipDelegateImsConfiguration;
import android.telephony.ims.SipDelegateManager;
import android.telephony.ims.SipMessage;
import android.telephony.ims.stub.SipDelegate;
import android.text.TextUtils;
import android.util.Log;

import com.android.internal.telephony.SipMessageParsingUtils;

import java.util.ArrayList;
import java.util.Set;
@@ -40,6 +44,7 @@ import java.util.concurrent.Executor;
 * @hide
 */
public class SipDelegateAidlWrapper implements DelegateStateCallback, DelegateMessageCallback {
    private static final String LOG_TAG = "SipDelegateAW";

    private final ISipDelegate.Stub mDelegateBinder = new ISipDelegate.Stub() {
        @Override
@@ -183,11 +188,15 @@ public class SipDelegateAidlWrapper implements DelegateStateCallback, DelegateMe
    }

    private void notifyLocalMessageFailedToBeReceived(SipMessage m, int reason) {
        //TODO: parse transaction ID or throw IllegalArgumentException if the SipMessage
        // transaction ID can not be parsed.
        String transactionId = SipMessageParsingUtils.getTransactionId(m.getHeaderSection());
        if (TextUtils.isEmpty(transactionId)) {
            Log.w(LOG_TAG, "failure to parse SipMessage.");
            throw new IllegalArgumentException("Malformed SipMessage, can not determine "
                    + "transaction ID.");
        }
        SipDelegate d = mDelegate;
        if (d != null) {
            mExecutor.execute(() -> d.notifyMessageReceiveError(null, reason));
            mExecutor.execute(() -> d.notifyMessageReceiveError(transactionId, reason));
        }
    }
}
+10 −3
Original line number Diff line number Diff line
@@ -28,9 +28,12 @@ import android.telephony.ims.SipMessage;
import android.telephony.ims.stub.DelegateConnectionMessageCallback;
import android.telephony.ims.stub.DelegateConnectionStateCallback;
import android.telephony.ims.stub.SipDelegate;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;

import com.android.internal.telephony.SipMessageParsingUtils;

import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.Executor;
@@ -265,9 +268,13 @@ public class SipDelegateConnectionAidlWrapper implements SipDelegateConnection,
    }

    private void notifyLocalMessageFailedToSend(SipMessage m, int reason) {
        //TODO: parse transaction ID or throw IllegalArgumentException if the SipMessage
        // transaction ID can not be parsed.
        String transactionId = SipMessageParsingUtils.getTransactionId(m.getHeaderSection());
        if (TextUtils.isEmpty(transactionId)) {
            Log.w(LOG_TAG, "sendMessage detected a malformed SipMessage and can not get a "
                    + "transaction ID.");
            throw new IllegalArgumentException("Could not send SipMessage due to malformed header");
        }
        mExecutor.execute(() ->
                mMessageCallback.onMessageSendFailure(null, reason));
                mMessageCallback.onMessageSendFailure(transactionId, reason));
    }
}