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 android.telephony.ims; 18 19 import static java.nio.charset.StandardCharsets.UTF_8; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.SystemApi; 24 import android.os.Build; 25 import android.os.Parcel; 26 import android.os.Parcelable; 27 import android.text.TextUtils; 28 29 import com.android.internal.telephony.SipMessageParsingUtils; 30 31 import java.util.Arrays; 32 import java.util.Objects; 33 34 /** 35 * Represents a partially encoded SIP message. See RFC 3261 for more information on how SIP 36 * messages are structured and used. 37 * <p> 38 * The SIP message is represented in a partially encoded form in order to allow for easier 39 * verification and should not be used as a generic SIP message container. 40 * @hide 41 */ 42 @SystemApi 43 public final class SipMessage implements Parcelable { 44 // Should not be set to true for production! 45 private static final boolean IS_DEBUGGING = Build.IS_ENG; 46 private static final String CRLF = "\r\n"; 47 48 private final String mStartLine; 49 private final String mHeaderSection; 50 private final byte[] mContent; 51 private final String mViaBranchParam; 52 private final String mCallIdParam; 53 54 /** 55 * Represents a partially encoded SIP message. 56 * 57 * @param startLine The start line of the message, containing either the request-line or 58 * status-line. 59 * @param headerSection A String containing the full unencoded SIP message header. 60 * @param content SIP message body. 61 */ SipMessage(@onNull String startLine, @NonNull String headerSection, @NonNull byte[] content)62 public SipMessage(@NonNull String startLine, @NonNull String headerSection, 63 @NonNull byte[] content) { 64 Objects.requireNonNull(startLine, "Required parameter is null: startLine"); 65 Objects.requireNonNull(headerSection, "Required parameter is null: headerSection"); 66 Objects.requireNonNull(content, "Required parameter is null: content"); 67 68 mStartLine = startLine; 69 mHeaderSection = headerSection; 70 mContent = content; 71 72 mViaBranchParam = SipMessageParsingUtils.getTransactionId(mHeaderSection); 73 if (TextUtils.isEmpty(mViaBranchParam)) { 74 throw new IllegalArgumentException("header section MUST contain a branch parameter " 75 + "inside of the Via header."); 76 } 77 mCallIdParam = SipMessageParsingUtils.getCallId(mHeaderSection); 78 } 79 80 /** 81 * Private constructor used only for unparcelling. 82 */ SipMessage(Parcel source)83 private SipMessage(Parcel source) { 84 mStartLine = source.readString(); 85 mHeaderSection = source.readString(); 86 mContent = new byte[source.readInt()]; 87 source.readByteArray(mContent); 88 mViaBranchParam = source.readString(); 89 mCallIdParam = source.readString(); 90 } 91 92 /** 93 * @return The start line of the SIP message, which contains either the request-line or 94 * status-line. 95 */ getStartLine()96 public @NonNull String getStartLine() { 97 return mStartLine; 98 } 99 100 /** 101 * @return The full, unencoded header section of the SIP message. 102 */ getHeaderSection()103 public @NonNull String getHeaderSection() { 104 return mHeaderSection; 105 } 106 107 /** 108 * @return the SIP message body. 109 */ getContent()110 public @NonNull byte[] getContent() { 111 return mContent; 112 } 113 114 /** 115 * @return the branch parameter enclosed in the Via header key's value. See RFC 3261 section 116 * 20.42 for more information on the Via header. 117 */ getViaBranchParameter()118 public @NonNull String getViaBranchParameter() { 119 return mViaBranchParam; 120 } 121 122 /** 123 * @return the value associated with the call-id header of this SIP message. See RFC 3261 124 * section 20.8 for more information on the call-id header. If {@code null}, then there was no 125 * call-id header found in this SIP message's headers. 126 */ getCallIdParameter()127 public @Nullable String getCallIdParameter() { 128 return mCallIdParam; 129 } 130 131 @Override describeContents()132 public int describeContents() { 133 return 0; 134 } 135 136 @Override writeToParcel(@onNull Parcel dest, int flags)137 public void writeToParcel(@NonNull Parcel dest, int flags) { 138 dest.writeString(mStartLine); 139 dest.writeString(mHeaderSection); 140 dest.writeInt(mContent.length); 141 dest.writeByteArray(mContent); 142 dest.writeString(mViaBranchParam); 143 dest.writeString(mCallIdParam); 144 } 145 146 public static final @NonNull Creator<SipMessage> CREATOR = new Creator<SipMessage>() { 147 @Override 148 public SipMessage createFromParcel(Parcel source) { 149 return new SipMessage(source); 150 } 151 152 @Override 153 public SipMessage[] newArray(int size) { 154 return new SipMessage[size]; 155 } 156 }; 157 158 @Override toString()159 public String toString() { 160 StringBuilder b = new StringBuilder(); 161 b.append("StartLine: ["); 162 if (IS_DEBUGGING) { 163 b.append(mStartLine); 164 } else { 165 b.append(sanitizeStartLineRequest(mStartLine)); 166 } 167 b.append("], Header: ["); 168 if (IS_DEBUGGING) { 169 b.append(mHeaderSection); 170 } else { 171 // only identify transaction id/call ID when it is available. 172 b.append("***"); 173 } 174 b.append("], Content: "); 175 b.append(getContent().length == 0 ? "[NONE]" : "[NOT SHOWN]"); 176 return b.toString(); 177 } 178 179 /** 180 * Detect if this is a REQUEST and redact Request-URI portion here, as it contains PII. 181 */ sanitizeStartLineRequest(String startLine)182 private String sanitizeStartLineRequest(String startLine) { 183 if (!SipMessageParsingUtils.isSipRequest(startLine)) return startLine; 184 String[] splitLine = startLine.split(" "); 185 return splitLine[0] + " <Request-URI> " + splitLine[2]; 186 } 187 188 @Override equals(Object o)189 public boolean equals(Object o) { 190 if (this == o) return true; 191 if (o == null || getClass() != o.getClass()) return false; 192 SipMessage that = (SipMessage) o; 193 return mStartLine.equals(that.mStartLine) 194 && mHeaderSection.equals(that.mHeaderSection) 195 && Arrays.equals(mContent, that.mContent); 196 } 197 198 @Override hashCode()199 public int hashCode() { 200 int result = Objects.hash(mStartLine, mHeaderSection); 201 result = 31 * result + Arrays.hashCode(mContent); 202 return result; 203 } 204 205 /** 206 * According RFC-3261 section 7, SIP is a text protocol and uses the UTF-8 charset. Its format 207 * consists of a start-line, one or more header fields, an empty line indicating the end of the 208 * header fields, and an optional message-body. 209 * 210 * <p> 211 * Returns a byte array with UTF-8 format representation of the encoded SipMessage. 212 * 213 * @return byte array with UTF-8 format representation of the encoded SipMessage. 214 */ toEncodedMessage()215 public @NonNull byte[] toEncodedMessage() { 216 byte[] header = new StringBuilder() 217 .append(mStartLine) 218 .append(mHeaderSection) 219 .append(CRLF) 220 .toString().getBytes(UTF_8); 221 byte[] sipMessage = new byte[header.length + mContent.length]; 222 System.arraycopy(header, 0, sipMessage, 0, header.length); 223 System.arraycopy(mContent, 0, sipMessage, header.length, mContent.length); 224 return sipMessage; 225 } 226 } 227