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.services.telephony.rcs;
18 
19 import android.telephony.ims.SipMessage;
20 import android.util.ArrayMap;
21 import android.util.ArraySet;
22 import android.util.LocalLog;
23 import android.util.Log;
24 
25 import com.android.internal.annotations.VisibleForTesting;
26 import com.android.internal.telephony.SipMessageParsingUtils;
27 import com.android.internal.telephony.metrics.RcsStats;
28 import com.android.internal.util.IndentingPrintWriter;
29 
30 import java.io.PrintWriter;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.Collections;
34 import java.util.List;
35 import java.util.Set;
36 import java.util.stream.Collectors;
37 
38 /**
39  * Tracks the state of SIP sessions started by a SIP INVITE (see RFC 3261)
40  * <p>
41  * Each SIP session created will consist of one or more SIP with, each dialog in the session
42  * having the same call-ID. Each SIP dialog will be in one of three states: EARLY, CONFIRMED, and
43  * CLOSED.
44  * <p>
45  * The SIP session will be closed once all of the associated dialogs are closed.
46  */
47 public class SipSessionTracker {
48     private static final String TAG = "SessionT";
49 
50     /**
51      * SIP request methods that will start a new SIP Dialog and move it into the PENDING state
52      * while we wait for a response. Note: INVITE is not the only SIP dialog that will create a
53      * dialog, however it is the only one that we wish to track for this use case.
54      */
55     public static final String[] SIP_REQUEST_DIALOG_START_METHODS = new String[] { "invite" };
56 
57     /**
58      * The SIP request method that will close a SIP Dialog in the ACTIVE state with the same
59      * Call-Id.
60      */
61     private static final String SIP_CLOSE_DIALOG_REQUEST_METHOD = "bye";
62 
63     private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE);
64     private final ArrayList<SipDialog> mTrackedDialogs = new ArrayList<>();
65     // Operations that are pending an ack from the remote application processing the message before
66     // they can be applied here. Maps the via header branch parameter of the message to the
67     // associated pending operation.
68     private final ArrayMap<String, Runnable> mPendingAck = new ArrayMap<>();
69 
70     private final RcsStats mRcsStats;
71     int mSubId;
72 
SipSessionTracker(int subId, RcsStats rcsStats)73     public SipSessionTracker(int subId, RcsStats rcsStats) {
74         mSubId = subId;
75         mRcsStats = rcsStats;
76     }
77 
78     /**
79      * Filter a SIP message to determine if it will result in a new SIP dialog. This will need to be
80      * successfully acknowledged by the remote IMS stack using
81      * {@link #acknowledgePendingMessage(String)} before we do any further processing.
82      *
83      * @param message The Incoming SIP message.
84      */
filterSipMessage(int direction, SipMessage message)85     public void filterSipMessage(int direction, SipMessage message) {
86         final Runnable r;
87         if (startsEarlyDialog(message)) {
88             r = getCreateDialogRunnable(direction, message);
89         } else if (closesDialog(message)) {
90             r = getCloseDialogRunnable(message);
91         } else if (SipMessageParsingUtils.isSipResponse(message.getStartLine())) {
92             r = getDialogStateChangeRunnable(message);
93         } else {
94             r = null;
95         }
96 
97         if (r != null) {
98             if (mPendingAck.containsKey(message.getViaBranchParameter())) {
99                 Runnable lastEvent = mPendingAck.get(message.getViaBranchParameter());
100                 logw("Adding new message when there was already a pending event for branch: "
101                         + message.getViaBranchParameter());
102                 Runnable concatRunnable = () -> {
103                     // No choice but to concatenate the Runnables together.
104                     if (lastEvent != null) lastEvent.run();
105                     r.run();
106                 };
107                 mPendingAck.put(message.getViaBranchParameter(), concatRunnable);
108             } else {
109                 mPendingAck.put(message.getViaBranchParameter(), r);
110             }
111         }
112     }
113 
114     /**
115      * The pending SIP message has been received by the remote IMS stack. We can now track dialogs
116      * associated with this message.
117      * message.
118      * @param viaBranchId The SIP message's Via header's branch parameter, which is used as a
119      *                    unique token.
120      */
acknowledgePendingMessage(String viaBranchId)121     public void acknowledgePendingMessage(String viaBranchId) {
122         Runnable r = mPendingAck.get(viaBranchId);
123         if (r != null) {
124             mPendingAck.remove(viaBranchId);
125             r.run();
126         }
127     }
128 
129     /**
130      * The pending SIP message has failed to be sent to the remote so remove the pending task.
131      * @param viaBranchId The failed message's Via header's branch parameter.
132      */
pendingMessageFailed(String viaBranchId)133     public void pendingMessageFailed(String viaBranchId) {
134         mPendingAck.remove(viaBranchId);
135     }
136 
137     /**
138      * A SIP session tracked by the remote application's IMS stack has been closed, so we can stop
139      * tracking it.
140      * @param callId The callId of the SIP session that has been closed.
141      */
cleanupSession(String callId)142     public void cleanupSession(String callId) {
143         List<SipDialog> dialogsToCleanup = mTrackedDialogs.stream()
144                 .filter(d -> d.getCallId().equals(callId))
145                 .collect(Collectors.toList());
146         if (dialogsToCleanup.isEmpty()) return;
147         logi("Cleanup dialogs associated with call id: " + callId);
148         for (SipDialog d : dialogsToCleanup) {
149             mRcsStats.onSipTransportSessionClosed(mSubId, callId, 0,
150                     d.getState() == d.STATE_CLOSED);
151             d.close();
152             logi("Dialog closed: " + d);
153         }
154         mTrackedDialogs.removeAll(dialogsToCleanup);
155     }
156 
157     /**
158      * @return the call IDs of the dialogs associated with the provided feature tags.
159      */
getCallIdsAssociatedWithFeatureTag(Set<String> featureTags)160     public Set<String> getCallIdsAssociatedWithFeatureTag(Set<String> featureTags) {
161         if (featureTags.isEmpty()) return Collections.emptySet();
162         Set<String> associatedIds = new ArraySet<>();
163         for (String featureTag : featureTags) {
164             for (SipDialog dialog : mTrackedDialogs) {
165                 boolean isAssociated = dialog.getAcceptContactFeatureTags().stream().anyMatch(
166                         d -> d.equalsIgnoreCase(featureTag));
167                 if (isAssociated) associatedIds.add(dialog.getCallId());
168             }
169         }
170         return associatedIds;
171     }
172 
173     /**
174      * @return All dialogs that have not received a final response yet 2XX or 3XX+.
175      */
getEarlyDialogs()176     public Set<SipDialog> getEarlyDialogs() {
177         return mTrackedDialogs.stream().filter(d -> d.getState() == SipDialog.STATE_EARLY)
178                 .collect(Collectors.toSet());
179     }
180 
181     /**
182      * @return All confirmed dialogs that have received a 2XX response and are active.
183      */
getConfirmedDialogs()184     public Set<SipDialog> getConfirmedDialogs() {
185         return mTrackedDialogs.stream().filter(d -> d.getState() == SipDialog.STATE_CONFIRMED)
186                 .collect(Collectors.toSet());
187     }
188 
189     /**
190      * @return Dialogs that have been closed via a BYE or 3XX+ response and
191      * {@link #cleanupSession(String)} has not been called yet.
192      */
193     @VisibleForTesting
getClosedDialogs()194     public Set<SipDialog> getClosedDialogs() {
195         return mTrackedDialogs.stream().filter(d -> d.getState() == SipDialog.STATE_CLOSED)
196                 .collect(Collectors.toSet());
197     }
198 
199     /**
200      * @return All of the tracked dialogs, even the ones that have been closed but
201      * {@link #cleanupSession(String)} has not been called.
202      */
getTrackedDialogs()203     public Set<SipDialog> getTrackedDialogs() {
204         return new ArraySet<>(mTrackedDialogs);
205     }
206 
207     /**
208      * Clears all tracked sessions.
209      */
clearAllSessions()210     public void clearAllSessions() {
211         for (SipDialog d : mTrackedDialogs) {
212             mRcsStats.onSipTransportSessionClosed(mSubId, d.getCallId(), 0, false);
213         }
214         mTrackedDialogs.clear();
215         mPendingAck.clear();
216     }
217 
218     /**
219      * Dump the state of this tracker to the provided PrintWriter.
220      */
dump(PrintWriter printWriter)221     public void dump(PrintWriter printWriter) {
222         IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
223         pw.println("SipSessionTracker:");
224         pw.increaseIndent();
225         pw.print("Early Call IDs: ");
226         pw.println(getEarlyDialogs().stream().map(SipDialog::getCallId)
227                 .collect(Collectors.toSet()));
228         pw.print("Confirmed Call IDs: ");
229         pw.println(getConfirmedDialogs().stream().map(SipDialog::getCallId)
230                 .collect(Collectors.toSet()));
231         pw.print("Closed Call IDs: ");
232         pw.println(getClosedDialogs().stream().map(SipDialog::getCallId)
233                 .collect(Collectors.toSet()));
234         pw.println("Tracked Dialogs:");
235         pw.increaseIndent();
236         for (SipDialog d : mTrackedDialogs) {
237             pw.println(d);
238         }
239         pw.decreaseIndent();
240         pw.println();
241         pw.println("Local Logs");
242         mLocalLog.dump(pw);
243         pw.decreaseIndent();
244     }
245 
246     /**
247      * @return {@code true}, if the SipMessage passed in should start a new SIP dialog,
248      * {@code false} if it should not.
249      */
startsEarlyDialog(SipMessage m)250     private boolean startsEarlyDialog(SipMessage m) {
251         if (!SipMessageParsingUtils.isSipRequest(m.getStartLine())) {
252             return false;
253         }
254         String[] startLineSegments = SipMessageParsingUtils.splitStartLineAndVerify(
255                 m.getStartLine());
256         if (startLineSegments == null) {
257             return false;
258         }
259         return Arrays.stream(SIP_REQUEST_DIALOG_START_METHODS)
260                 .anyMatch(r -> r.equalsIgnoreCase(startLineSegments[0]));
261     }
262 
263     /**
264      * @return {@code true}, if the SipMessage passed in should close a confirmed dialog,
265      * {@code false} if it should not.
266      */
closesDialog(SipMessage m)267     private boolean closesDialog(SipMessage m) {
268         if (!SipMessageParsingUtils.isSipRequest(m.getStartLine())) {
269             return false;
270         }
271         String[] startLineSegments = SipMessageParsingUtils.splitStartLineAndVerify(
272                 m.getStartLine());
273         if (startLineSegments == null) {
274             return false;
275         }
276         return SIP_CLOSE_DIALOG_REQUEST_METHOD.equalsIgnoreCase(startLineSegments[0]);
277     }
278 
getCreateDialogRunnable(int direction, SipMessage m)279     private Runnable getCreateDialogRunnable(int direction, SipMessage m) {
280         return () -> {
281             List<SipDialog> duplicateDialogs = mTrackedDialogs.stream()
282                     .filter(d -> d.getCallId().equals(m.getCallIdParameter()))
283                     .collect(Collectors.toList());
284             if (duplicateDialogs.size() > 0) {
285                 logi("trying to create a dialog for a call ID that already exists, skip: "
286                         + duplicateDialogs);
287                 return;
288             }
289             SipDialog dialog = SipDialog.fromSipMessage(m);
290             String[] startLineSegments =
291                     SipMessageParsingUtils.splitStartLineAndVerify(m.getStartLine());
292             mRcsStats.earlySipTransportSession(startLineSegments[0], dialog.getCallId(),
293                     direction);
294             logi("Starting new SipDialog: " + dialog);
295             mTrackedDialogs.add(dialog);
296         };
297     }
298 
getCloseDialogRunnable(SipMessage m)299     private Runnable getCloseDialogRunnable(SipMessage m) {
300         return () -> {
301             List<SipDialog> dialogsToClose = mTrackedDialogs.stream()
302                     .filter(d -> d.isRequestAssociatedWithDialog(m))
303                     .collect(Collectors.toList());
304             if (dialogsToClose.isEmpty()) return;
305             logi("Closing dialogs associated with: " + m);
306             mRcsStats.onSipTransportSessionClosed(mSubId, m.getCallIdParameter(), 0, true);
307             for (SipDialog d : dialogsToClose) {
308                 d.close();
309                 logi("Dialog closed: " + d);
310             }
311         };
312     }
313 
314     private Runnable getDialogStateChangeRunnable(SipMessage m) {
315         return () -> {
316             // This will return a dialog and all of its potential forks
317             List<SipDialog> associatedDialogs = mTrackedDialogs.stream()
318                     .filter(d -> d.isResponseAssociatedWithDialog(m))
319                     .collect(Collectors.toList());
320             if (associatedDialogs.isEmpty()) return;
321             String messageToTag = SipMessageParsingUtils.getToTag(m.getHeaderSection());
322             // If the to tag matches (or message to tag doesn't exist in dialog yet because this is
323             // the first response), then we are done.
324             SipDialog match = associatedDialogs.stream()
325                     .filter(d -> d.getToTag() == null || d.getToTag().equals(messageToTag))
326                     .findFirst().orElse(null);
327             if (match == null) {
328                 // If it doesn't then we have a situation where we need to fork the existing dialog.
329                 // The dialog used to fork doesn't matter, since the required params are the same,
330                 // so simply use the first one in the returned list.
331                 logi("Dialog forked");
332                 match = associatedDialogs.get(0).forkDialog();
333                 mTrackedDialogs.add(match);
334             }
335             if (match != null) {
336                 logi("Dialog: " + match + " is associated with: " + m);
337                 updateSipDialogState(match, m);
338                 logi("Dialog state updated to " + match);
339             } else {
340                 logi("No Dialogs are associated with: " + m);
341             }
342         };
343     }
344 
345     private void updateSipDialogState(SipDialog d, SipMessage m) {
346         String[] startLineSegments = SipMessageParsingUtils.splitStartLineAndVerify(
347                 m.getStartLine());
348         if (startLineSegments == null) {
349             logw("Could not parse start line for SIP message: " + m.getStartLine());
350             return;
351         }
352         int statusCode = 0;
353         try {
354             statusCode = Integer.parseInt(startLineSegments[1]);
355         } catch (NumberFormatException e) {
356             logw("Could not parse status code for SIP message: " + m.getStartLine());
357             return;
358         }
359         String toTag = SipMessageParsingUtils.getToTag(m.getHeaderSection());
360         logi("updateSipDialogState: message has statusCode: " + statusCode + ", and to tag: "
361                 + toTag);
362         // If specifically 100 Trying, then do not do anything.
363         if (statusCode <= 100) return;
364         // If 300+, then this dialog has received an error response and should move to closed state.
365         if (statusCode >= 300) {
366             mRcsStats.onSipTransportSessionClosed(mSubId, m.getCallIdParameter(), statusCode, true);
367             d.close();
368             return;
369         }
370         if (toTag == null) logw("updateSipDialogState: No to tag for message: " + m);
371         if (statusCode >= 200) {
372             mRcsStats.confirmedSipTransportSession(m.getCallIdParameter(), statusCode);
373             d.confirm(toTag);
374             return;
375         }
376         // 1XX responses still require updates to dialogs.
377         d.earlyResponse(toTag);
378     }
379 
380     private void logi(String log) {
381         Log.i(SipTransportController.LOG_TAG, TAG + ": " + log);
382         mLocalLog.log("[I] " + log);
383     }
384 
385     private void logw(String log) {
386         Log.w(SipTransportController.LOG_TAG, TAG + ": " + log);
387         mLocalLog.log("[W] " + log);
388     }
389 }
390