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.libraries.rcs.simpleclient.service.chat;
18 
19 import static com.android.libraries.rcs.simpleclient.protocol.cpim.CpimUtils.CPIM_CONTENT_TYPE;
20 import static com.android.libraries.rcs.simpleclient.service.chat.ChatServiceException.CODE_ERROR_SEND_MESSAGE_FAILED;
21 import static com.android.libraries.rcs.simpleclient.service.chat.ChatServiceException.CODE_ERROR_UNSPECIFIED;
22 
23 import static java.nio.charset.StandardCharsets.UTF_8;
24 
25 import android.text.TextUtils;
26 import android.util.Log;
27 
28 import androidx.annotation.Nullable;
29 
30 import com.android.libraries.rcs.simpleclient.SimpleRcsClientContext;
31 import com.android.libraries.rcs.simpleclient.protocol.cpim.CpimUtils;
32 import com.android.libraries.rcs.simpleclient.protocol.cpim.SimpleCpimMessage;
33 import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunk;
34 import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunk.Continuation;
35 import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunkHeader;
36 import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpConstants;
37 import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpManager;
38 import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpSession;
39 import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpUtils;
40 import com.android.libraries.rcs.simpleclient.protocol.sdp.SdpUtils;
41 import com.android.libraries.rcs.simpleclient.protocol.sdp.SimpleSdpMessage;
42 import com.android.libraries.rcs.simpleclient.protocol.sip.SipSessionConfiguration;
43 import com.android.libraries.rcs.simpleclient.protocol.sip.SipUtils;
44 import com.android.libraries.rcs.simpleclient.service.chat.ChatServiceException.ErrorCode;
45 
46 import com.google.common.util.concurrent.FutureCallback;
47 import com.google.common.util.concurrent.Futures;
48 import com.google.common.util.concurrent.ListenableFuture;
49 import com.google.common.util.concurrent.MoreExecutors;
50 import com.google.common.util.concurrent.SettableFuture;
51 
52 import gov.nist.javax.sip.header.To;
53 import gov.nist.javax.sip.header.ims.PAssertedIdentityHeader;
54 import gov.nist.javax.sip.message.SIPRequest;
55 import gov.nist.javax.sip.message.SIPResponse;
56 
57 import java.io.ByteArrayInputStream;
58 import java.io.IOException;
59 import java.text.ParseException;
60 import java.util.UUID;
61 
62 import javax.sip.address.URI;
63 import javax.sip.message.Message;
64 import javax.sip.message.Request;
65 import javax.sip.message.Response;
66 
67 /**
68  * Simple chat session implementation in order to send/receive a text message via SIP/MSRP
69  * connection. Currently, this supports only a outgoing CPM session.
70  */
71 public class SimpleChatSession {
72     private static final String TAG = SimpleChatSession.class.getSimpleName();
73     private final SimpleRcsClientContext mContext;
74     private final MinimalCpmChatService mService;
75     private final MsrpManager mMsrpManager;
76     private final String mConversationId = UUID.randomUUID().toString();
77     private SettableFuture<Void> mStartFuture;
78     @Nullable
79     private SIPRequest mInviteRequest;
80     @Nullable
81     private URI mRemoteUri;
82     @Nullable
83     private SimpleSdpMessage mRemoteSdp;
84     @Nullable
85     private SimpleSdpMessage mLocalSdp;
86     @Nullable
87     private MsrpSession mMsrpSession;
88     @Nullable
89     private ChatSessionListener mListener;
90 
91 
SimpleChatSession( SimpleRcsClientContext context, MinimalCpmChatService service, MsrpManager msrpManager)92     public SimpleChatSession(
93             SimpleRcsClientContext context, MinimalCpmChatService service,
94             MsrpManager msrpManager) {
95         mService = service;
96         mContext = context;
97         mMsrpManager = msrpManager;
98     }
99 
getRemoteUri()100     public URI getRemoteUri() {
101         return mRemoteUri;
102     }
103 
104     /** Send a text message via MSRP session associated with this session. */
sendMessage(String msg)105     public ListenableFuture<Void> sendMessage(String msg) {
106         MsrpSession session = mMsrpSession;
107         if (session == null || mRemoteSdp == null || mLocalSdp == null) {
108             Log.e(TAG, "Session is not established");
109             return Futures.immediateFailedFuture(
110                     new IllegalStateException("Session is not established"));
111         }
112 
113         // Build a new CPIM message and send it out through the MSRP session.
114         SimpleCpimMessage cpim = CpimUtils.createForText(msg);
115         Log.i(TAG, "Encoded CPIM:" + cpim.encode());
116 
117         byte[] content = cpim.encode().getBytes(UTF_8);
118         MsrpChunk msrpChunk =
119                 MsrpChunk.newBuilder()
120                         .method(MsrpChunk.Method.SEND)
121                         .transactionId(MsrpUtils.generateRandomId())
122                         .content(content)
123                         .continuation(Continuation.COMPLETE)
124                         .addHeader(MsrpConstants.HEADER_TO_PATH, mRemoteSdp.getPath().get())
125                         .addHeader(MsrpConstants.HEADER_FROM_PATH, mLocalSdp.getPath().get())
126                         .addHeader(MsrpConstants.HEADER_FAILURE_REPORT,
127                                 MsrpConstants.REPORT_VALUE_YES)
128                         .addHeader(MsrpConstants.HEADER_SUCCESS_REPORT,
129                                 MsrpConstants.REPORT_VALUE_NO)
130                         .addHeader(
131                                 MsrpConstants.HEADER_BYTE_RANGE,
132                                 String.format("1-%d/%d", content.length, content.length))
133                         .addHeader(MsrpConstants.HEADER_MESSAGE_ID, MsrpUtils.generateRandomId())
134                         .addHeader(MsrpConstants.HEADER_CONTENT_TYPE, CPIM_CONTENT_TYPE)
135                         .build();
136 
137         Log.i(TAG, "Send a MSRP chunk: " + msrpChunk);
138         return Futures.transformAsync(session.send(msrpChunk), result -> {
139             if (result == null) {
140                 return Futures.immediateFailedFuture(
141                         new ChatServiceException("Failed to send a chunk",
142                                 CODE_ERROR_SEND_MESSAGE_FAILED));
143             }
144             if (result.responseCode() != 200) {
145                 Log.d(TAG, "Received error response id=" + result.transactionId()
146                         + " code=" + result.responseCode());
147                 return Futures.immediateFailedFuture(
148                         new ChatServiceException("Msrp response code: " + result.responseCode(),
149                                 CODE_ERROR_SEND_MESSAGE_FAILED));
150             }
151             return Futures.immediateFuture(null);
152         }, MoreExecutors.directExecutor());
153     }
154 
155     /** Start outgoing chat session. */
156     ListenableFuture<Void> start(String telUriContact) {
157         if (mStartFuture != null) {
158             return Futures.immediateFailedFuture(
159                     new ChatServiceException("Session already started"));
160         }
161 
162         SettableFuture<Void> future = SettableFuture.create();
163         mStartFuture = future;
164         mRemoteUri = SipUtils.createUri(telUriContact);
165         try {
166             SipSessionConfiguration configuration =
167                     mContext.getSipSession().getSessionConfiguration();
168             SimpleSdpMessage sdp = SdpUtils.createSdpForMsrp(configuration.getLocalIpAddress(),
169                     false);
170             SIPRequest invite =
171                     SipUtils.buildInvite(
172                             mContext.getSipSession().getSessionConfiguration(),
173                             telUriContact,
174                             mConversationId,
175                             sdp.encode().getBytes(UTF_8));
176             mInviteRequest = invite;
177             mLocalSdp = sdp;
178             Futures.addCallback(
179                     mService.sendSipRequest(invite, this),
180                     new FutureCallback<Boolean>() {
181                         @Override
182                         public void onSuccess(Boolean result) {
183                             Log.i(TAG, "onSuccess:" + result);
184                             if (!result) {
185                                 notifyFailure("Failed to send INVITE", CODE_ERROR_UNSPECIFIED);
186                             }
187                         }
188 
189                         @Override
190                         public void onFailure(Throwable t) {
191                             Log.i(TAG, "onFailure:" + t.getMessage());
192                             notifyFailure("Failed to send INVITE", CODE_ERROR_UNSPECIFIED);
193                         }
194                     },
195                     MoreExecutors.directExecutor());
196         } catch (ParseException e) {
197             Log.e(TAG, e.getMessage());
198             e.printStackTrace();
199             return Futures.immediateFailedFuture(
200                     new ChatServiceException("Failed to build INVITE"));
201         }
202 
203         return future;
204     }
205 
206     /** Start incoming chat session. */
207     ListenableFuture<Void> start(SIPRequest invite) {
208         mInviteRequest = invite;
209         int statusCode = Response.OK;
210         if (!SipUtils.hasSdpContent(invite)) {
211             statusCode = Response.NOT_ACCEPTABLE_HERE;
212         } else {
213             try {
214                 mRemoteSdp = SimpleSdpMessage.parse(
215                         new ByteArrayInputStream(invite.getRawContent()));
216             } catch (ParseException | IOException e) {
217                 statusCode = Response.BAD_REQUEST;
218             }
219         }
220 
221         updateRemoteUri(mInviteRequest);
222 
223         SipSessionConfiguration configuration = mContext.getSipSession().getSessionConfiguration();
224         SimpleSdpMessage sdp = SdpUtils.createSdpForMsrp(configuration.getLocalIpAddress(), false);
225 
226         // Automatically reply back to the invite by building a pre-canned response.
227         try {
228             SIPResponse response = SipUtils.buildInviteResponse(configuration, invite, statusCode,
229                     sdp);
230             mLocalSdp = sdp;
231             return Futures.transform(
232                     mService.sendSipResponse(response, this), result -> null,
233                     MoreExecutors.directExecutor());
234         } catch (ParseException e) {
235             Log.e(TAG, "Exception while building response", e);
236             return Futures.immediateFailedFuture(e);
237         }
238     }
239 
240     /** Terminate the current SIP session. */
241     public ListenableFuture<Void> terminate() {
242         if (mInviteRequest == null) {
243             return Futures.immediateFuture(null);
244         }
245         try {
246             if (mMsrpSession != null) {
247                 mMsrpSession.terminate();
248             }
249         } catch (IOException e) {
250             return Futures.immediateFailedFuture(
251                     new ChatServiceException(
252                             "Exception while terminating MSRP session", CODE_ERROR_UNSPECIFIED));
253         }
254         try {
255 
256             SettableFuture<Void> future = SettableFuture.create();
257             Futures.addCallback(
258                     mService.sendSipRequest(SipUtils.buildBye(mInviteRequest), this),
259                     new FutureCallback<Boolean>() {
260                         @Override
261                         public void onSuccess(Boolean result) {
262                             future.set(null);
263                         }
264 
265                         @Override
266                         public void onFailure(Throwable t) {
267                             future.setException(
268                                     new ChatServiceException("Failed to send BYE",
269                                             CODE_ERROR_UNSPECIFIED, t));
270                         }
271                     },
272                     MoreExecutors.directExecutor());
273             return future;
274         } catch (ParseException e) {
275             return Futures.immediateFailedFuture(
276                     new ChatServiceException("Failed to build BYE", CODE_ERROR_UNSPECIFIED));
277         }
278     }
279 
280     void receiveMessage(Message msg) {
281         if (msg instanceof SIPRequest) {
282             handleSipRequest((SIPRequest) msg);
283         } else {
284             handleSipResponse((SIPResponse) msg);
285         }
286     }
287 
288     private void handleSipRequest(SIPRequest request) {
289         SIPResponse response;
290         if (TextUtils.equals(request.getMethod(), Request.ACK)) {
291             // Terminating session established, start a msrp session.
292             if (mRemoteSdp != null) {
293                 startMsrpSession(mRemoteSdp);
294             }
295             return;
296         }
297 
298         if (TextUtils.equals(request.getMethod(), Request.BYE)) {
299             response = request.createResponse(Response.OK);
300         } else {
301             // Currently we support only INVITE and BYE.
302             response = request.createResponse(Response.METHOD_NOT_ALLOWED);
303         }
304         Futures.addCallback(
305                 mService.sendSipResponse(response, this),
306                 new FutureCallback<Boolean>() {
307                     @Override
308                     public void onSuccess(Boolean result) {
309                         if (result) {
310                             Log.d(
311                                     TAG,
312                                     "Response to Call-Id: "
313                                             + response.getCallId().getCallId()
314                                             + " sent successfully");
315                         } else {
316                             Log.d(TAG, "Failed to send response");
317                         }
318                     }
319 
320                     @Override
321                     public void onFailure(Throwable t) {
322                         Log.d(TAG, "Exception while sending response: ", t);
323                     }
324                 },
325                 MoreExecutors.directExecutor());
326     }
327 
328     private void handleSipResponse(SIPResponse response) {
329         int code = response.getStatusCode();
330 
331         // Nothing to do for a provisional response.
332         if (response.isFinalResponse()) {
333             if (code == Response.OK) {
334                 handle200OK(response);
335             } else {
336                 handleNon200(response);
337             }
338         }
339     }
340 
341     private void handleNon200(SIPResponse response) {
342         Log.d(TAG, "Received error response code=" + response.getStatusCode());
343         notifyFailure("Received non-200 INVITE response", CODE_ERROR_UNSPECIFIED);
344     }
345 
346     private void handle200OK(SIPResponse response) {
347         if (!SipUtils.hasSdpContent(response)) {
348             notifyFailure("Content is not a SDP", CODE_ERROR_UNSPECIFIED);
349             return;
350         }
351 
352         SimpleSdpMessage sdp;
353         try {
354             sdp = SimpleSdpMessage.parse(new ByteArrayInputStream(response.getRawContent()));
355         } catch (ParseException | IOException e) {
356             notifyFailure("Invalid SDP in INVITE", CODE_ERROR_UNSPECIFIED);
357             return;
358         }
359 
360         if (mInviteRequest == null) {
361             notifyFailure("No INVITE request sent out", CODE_ERROR_UNSPECIFIED);
362             return;
363         }
364 
365         SIPRequest ack = mInviteRequest.createAckRequest((To) response.getToHeader());
366         Futures.addCallback(
367                 mService.sendSipRequest(ack, this),
368                 new FutureCallback<Boolean>() {
369                     @Override
370                     public void onSuccess(Boolean result) {
371                         if (result) {
372                             startMsrpSession(sdp);
373                             mRemoteSdp = sdp;
374                         } else {
375                             notifyFailure("Failed to send ACK", CODE_ERROR_UNSPECIFIED);
376                         }
377                     }
378 
379                     @Override
380                     public void onFailure(Throwable t) {
381                         notifyFailure("Failed to send ACK", CODE_ERROR_UNSPECIFIED);
382                     }
383                 },
384                 MoreExecutors.directExecutor());
385     }
386 
387     private void notifyFailure(String message, @ErrorCode int code) {
388         if (mStartFuture != null) {
389             mStartFuture.setException(new ChatServiceException(message, code));
390             mStartFuture = null;
391         }
392     }
393 
394     private void notifySuccess() {
395         if (mStartFuture != null) {
396             mStartFuture.set(null);
397             mStartFuture = null;
398         }
399     }
400 
401     private void startMsrpSession(SimpleSdpMessage remoteSdp) {
402         Log.d(TAG, "Start MSRP session: " + remoteSdp);
403         if (remoteSdp.getAddress().isPresent() && remoteSdp.getPort().isPresent()) {
404             String localIp = getLocalIp();
405             Futures.addCallback(
406                     mMsrpManager.createMsrpSession(
407                             remoteSdp.getAddress().get(), remoteSdp.getPort().getAsInt(), localIp,
408                             0 /* localPort */, this::receiveMsrpChunk),
409                     new FutureCallback<MsrpSession>() {
410                         @Override
411                         public void onSuccess(MsrpSession result) {
412                             mMsrpSession = result;
413                             sendEmptyPacket();
414                             notifySuccess();
415                         }
416 
417                         @Override
418                         public void onFailure(Throwable t) {
419                             Log.e(TAG, "Failed to create msrp session", t);
420                             notifyFailure("Failed to establish msrp session",
421                                     CODE_ERROR_UNSPECIFIED);
422                             terminate()
423                                     .addListener(
424                                             () -> Log.d(TAG, "Session terminated"),
425                                             MoreExecutors.directExecutor());
426                         }
427                     },
428                     MoreExecutors.directExecutor());
429         } else {
430             Log.e(TAG, "Address or port is not present");
431         }
432     }
433 
434     private void sendEmptyPacket() {
435         MsrpChunk msrpChunk =
436                 MsrpChunk.newBuilder()
437                         .method(MsrpChunk.Method.SEND)
438                         .transactionId(MsrpUtils.generateRandomId())
439                         .continuation(Continuation.COMPLETE)
440                         .addHeader(MsrpConstants.HEADER_TO_PATH, mRemoteSdp.getPath().get())
441                         .addHeader(MsrpConstants.HEADER_FROM_PATH, mLocalSdp.getPath().get())
442                         .addHeader(MsrpConstants.HEADER_FAILURE_REPORT,
443                                 MsrpConstants.REPORT_VALUE_NO)
444                         .addHeader(MsrpConstants.HEADER_SUCCESS_REPORT,
445                                 MsrpConstants.REPORT_VALUE_NO)
446                         .addHeader(MsrpConstants.HEADER_BYTE_RANGE, "1/0-0")
447                         .addHeader(MsrpConstants.HEADER_MESSAGE_ID, MsrpUtils.generateRandomId())
448                         .build();
449 
450         mMsrpSession.send(msrpChunk);
451     }
452 
453     private String getLocalIp() {
454         SipSessionConfiguration configuration = mContext.getSipSession().getSessionConfiguration();
455         return configuration.getLocalIpAddress();
456     }
457 
458     private void receiveMsrpChunk(MsrpChunk chunk) {
459         Log.d(TAG, "Received msrp= " + chunk + " conversation=" + mConversationId);
460 
461         MsrpChunkHeader contentTypeHeader = chunk.header("Content-Type");
462         if (chunk.content().length == 0 || contentTypeHeader == null) {
463             Log.i(TAG, "No content or Content-Type header, drop it");
464             return;
465         }
466 
467         String contentType = contentTypeHeader.value();
468         if ("message/cpim".equals(contentType)) {
469             Log.d(TAG, "Received CPIM: " + new String(chunk.content(), UTF_8));
470             try {
471                 SimpleCpimMessage cpim = SimpleCpimMessage.parse(chunk.content());
472                 if (mListener != null) {
473                     mListener.onMessageReceived(cpim);
474                 }
475             } catch (Exception e) {
476                 Log.e(TAG, "Error while parsing cpim message.", e);
477             }
478         } else {
479             Log.w(TAG, contentType + " is not supported.");
480         }
481     }
482 
483 
484     /** Set new listener for this session. */
485     public void setListener(@Nullable ChatSessionListener listener) {
486         mListener = listener;
487     }
488 
489     private void updateRemoteUri(SIPRequest request) {
490         PAssertedIdentityHeader pAssertedIdentityHeader =
491                 (PAssertedIdentityHeader) request.getHeader("P-Asserted-Identity");
492         if (pAssertedIdentityHeader == null) {
493             mRemoteUri = request.getFrom().getAddress().getURI();
494         } else {
495             mRemoteUri = pAssertedIdentityHeader.getAddress().getURI();
496         }
497     }
498 }
499