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.protocol.sip;
18 
19 import static com.android.libraries.rcs.simpleclient.protocol.sdp.SdpUtils.SDP_CONTENT_SUB_TYPE;
20 import static com.android.libraries.rcs.simpleclient.protocol.sdp.SdpUtils.SDP_CONTENT_TYPE;
21 
22 import android.text.TextUtils;
23 import android.util.Log;
24 
25 import androidx.annotation.Nullable;
26 
27 import com.android.libraries.rcs.simpleclient.protocol.sdp.SimpleSdpMessage;
28 
29 import com.google.common.base.Ascii;
30 import com.google.common.collect.ImmutableList;
31 import com.google.common.collect.Iterables;
32 import com.google.common.net.InetAddresses;
33 
34 import gov.nist.javax.sip.Utils;
35 import gov.nist.javax.sip.address.AddressFactoryImpl;
36 import gov.nist.javax.sip.header.ContentType;
37 import gov.nist.javax.sip.header.HeaderFactoryImpl;
38 import gov.nist.javax.sip.header.Via;
39 import gov.nist.javax.sip.header.extensions.SessionExpires;
40 import gov.nist.javax.sip.header.ims.PPreferredIdentityHeader;
41 import gov.nist.javax.sip.header.ims.PPreferredServiceHeader;
42 import gov.nist.javax.sip.header.ims.SecurityVerifyHeader;
43 import gov.nist.javax.sip.message.SIPMessage;
44 import gov.nist.javax.sip.message.SIPRequest;
45 import gov.nist.javax.sip.message.SIPResponse;
46 
47 import java.net.Inet6Address;
48 import java.text.ParseException;
49 import java.util.List;
50 import java.util.UUID;
51 
52 import javax.sip.InvalidArgumentException;
53 import javax.sip.address.AddressFactory;
54 import javax.sip.address.SipURI;
55 import javax.sip.address.URI;
56 import javax.sip.header.ContactHeader;
57 import javax.sip.header.Header;
58 import javax.sip.header.HeaderFactory;
59 import javax.sip.header.ViaHeader;
60 import javax.sip.message.Request;
61 import javax.sip.message.Response;
62 
63 /** Collections of utility functions for SIP */
64 public final class SipUtils {
65     private static final String TAG = "SipUtils";
66     private static final String SUPPORTED_TIMER_TAG = "timer";
67     private static final String ICSI_REF_PARAM_NAME = "+g.3gpp.icsi-ref";
68     private static final String SIP_INSTANCE_PARAM_NAME = "+sip.instance";
69     private static final String CPM_SESSION_FEATURE_TAG_PARAM_VALUE =
70             "\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"";
71     private static final String CPM_SESSION_FEATURE_TAG_FULL_STRING =
72             "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"";
73     private static final String CPM_SESSION_SERVICE_NAME =
74             "urn:urn-7:3gpp-service.ims.icsi.oma.cpm.session";
75     private static final String CONTRIBUTION_ID_HEADER_NAME = "Contribution-ID";
76     private static final String CONVERSATION_ID_HEADER_NAME = "Conversation-ID";
77     private static final String ACCEPT_CONTACT_HEADER_NAME = "Accept-Contact";
78     private static final String PANI_HEADER_NAME = "P-Access-Network-Info";
79     private static final String PLANI_HEADER_NAME = "P-Last-Access-Network-Info";
80     private static final String USER_AGENT_HEADER = "RcsTestClient";
81 
82     private static AddressFactory sAddressFactory = new AddressFactoryImpl();
83     private static HeaderFactory sHeaderFactory = new HeaderFactoryImpl();
84 
SipUtils()85     private SipUtils() {
86     }
87 
88     /**
89      * Try to parse the given uri.
90      *
91      * @throws IllegalArgumentException in case of parsing error.
92      */
createUri(String uri)93     public static URI createUri(String uri) {
94         try {
95             return sAddressFactory.createURI(uri);
96         } catch (ParseException exception) {
97             throw new IllegalArgumentException("URI cannot be created", exception);
98         }
99     }
100 
101     /**
102      * Create SIP INVITE request for a CPM 1:1 chat.
103      *
104      * @param configuration  The SipSessionConfiguration instance used for populating SIP headers.
105      * @param targetUri      The uri to be targeted.
106      * @param conversationId The id to be contained in Conversation-ID header.
107      */
buildInvite( SipSessionConfiguration configuration, String targetUri, String conversationId, byte[] content)108     public static SIPRequest buildInvite(
109             SipSessionConfiguration configuration,
110             String targetUri,
111             String conversationId,
112             byte[] content)
113             throws ParseException {
114         String address = configuration.getLocalIpAddress();
115         int port = configuration.getLocalPort();
116         String transport = configuration.getSipTransport();
117         List<String> associatedUris = configuration.getAssociatedUris();
118         String preferredUri = Iterables.getFirst(associatedUris,
119                 configuration.getPublicUserIdentity());
120 
121         SIPRequest request = new SIPRequest();
122         request.setMethod(Request.INVITE);
123 
124         URI remoteUri = createUri(targetUri);
125         request.setRequestURI(remoteUri);
126         request.setFrom(
127                 sHeaderFactory.createFromHeader(
128                         sAddressFactory.createAddress(preferredUri),
129                         Utils.getInstance().generateTag()));
130         request.setTo(
131                 sHeaderFactory.createToHeader(sAddressFactory.createAddress(remoteUri), null));
132 
133         ViaHeader viaHeader = null;
134 
135         try {
136             // Set a default Max-Forwards header.
137             request.setMaxForwards(sHeaderFactory.createMaxForwardsHeader(70));
138             request.setCSeq(sHeaderFactory.createCSeqHeader(1L, Request.INVITE));
139             viaHeader =
140                     sHeaderFactory.createViaHeader(
141                             address, port, transport, Utils.getInstance().generateBranchId());
142             request.setVia(ImmutableList.of(viaHeader));
143 
144             // Set a default Session-Expires header.
145             SessionExpires sessionExpires = new SessionExpires();
146             sessionExpires.setRefresher("uac");
147             sessionExpires.setExpires(1800);
148             request.setHeader(sessionExpires);
149 
150             // Set a Contact header.
151             request.setHeader(generateContactHeader(configuration));
152 
153             // Set PANI and PLANI if exists
154             if (configuration.getPaniHeader() != null) {
155                 request.setHeader(
156                         sHeaderFactory.createHeader(PANI_HEADER_NAME,
157                                 configuration.getPaniHeader()));
158             }
159             if (configuration.getPlaniHeader() != null) {
160                 request.setHeader(
161                         sHeaderFactory.createHeader(PLANI_HEADER_NAME,
162                                 configuration.getPlaniHeader()));
163             }
164         } catch (InvalidArgumentException e) {
165             // Nothing to do here
166             Log.e(TAG, e.getMessage());
167         }
168 
169         request.setCallId(UUID.randomUUID().toString());
170         request.setHeader(sHeaderFactory.createHeader(CONVERSATION_ID_HEADER_NAME, conversationId));
171         request.setHeader(
172                 sHeaderFactory.createHeader(CONTRIBUTION_ID_HEADER_NAME,
173                         UUID.randomUUID().toString()));
174 
175         String acceptContact = "*;" + CPM_SESSION_FEATURE_TAG_FULL_STRING;
176         request.setHeader(sHeaderFactory.createHeader(ACCEPT_CONTACT_HEADER_NAME, acceptContact));
177         request.setHeader(sHeaderFactory.createSupportedHeader(SUPPORTED_TIMER_TAG));
178         request.setHeader(sHeaderFactory.createHeader(PPreferredIdentityHeader.NAME, preferredUri));
179         request.setHeader(
180                 sHeaderFactory.createHeader(PPreferredServiceHeader.NAME,
181                         CPM_SESSION_SERVICE_NAME));
182 
183         // Set a Security-Verify header if exist.
184         String securityVerify = configuration.getSecurityVerifyHeader();
185         if (!TextUtils.isEmpty(securityVerify)) {
186             request.setHeaders(sHeaderFactory.createHeaders(SecurityVerifyHeader.NAME
187                     + ":" + securityVerify.trim()));
188         }
189 
190         // Add Route headers.
191         List<String> serviceRoutes = configuration.getServiceRouteHeaders();
192         if (!serviceRoutes.isEmpty()) {
193             for (String sr : serviceRoutes) {
194                 request.addHeader(
195                         sHeaderFactory.createRouteHeader(sAddressFactory.createAddress(sr)));
196             }
197         }
198 
199         String userAgent = configuration.getUserAgentHeader();
200         userAgent = (userAgent == null) ? USER_AGENT_HEADER : userAgent;
201         request.addHeader(sHeaderFactory.createUserAgentHeader(ImmutableList.of(userAgent)));
202 
203         request.setMessageContent(SDP_CONTENT_TYPE, SDP_CONTENT_SUB_TYPE, content);
204 
205         if (viaHeader != null && Ascii.equalsIgnoreCase("udp", transport)) {
206             String newTransport =
207                     determineTransportBySize(configuration, request.encodeAsBytes("udp").length);
208             if (!Ascii.equalsIgnoreCase(transport, newTransport)) {
209                 viaHeader.setTransport(newTransport);
210             }
211         }
212 
213         return request;
214     }
215 
generateContactHeader(SipSessionConfiguration configuration)216     private static ContactHeader generateContactHeader(SipSessionConfiguration configuration)
217             throws ParseException {
218         String host = configuration.getLocalIpAddress();
219         if (isIPv6Address(host)) {
220             host = "[" + host + "]";
221         }
222 
223         String userPart = configuration.getContactUser();
224         SipURI uri = sAddressFactory.createSipURI(userPart, host);
225         try {
226             uri.setPort(configuration.getLocalPort());
227             uri.setTransportParam(configuration.getSipTransport());
228         } catch (Exception e) {
229             // Shouldn't be here.
230         }
231 
232         ContactHeader contactHeader =
233                 sHeaderFactory.createContactHeader(sAddressFactory.createAddress(uri));
234 
235         // Add +sip.instance param.
236         String sipInstance = "\"<urn:gsma:imei:" + configuration.getImei() + ">\"";
237         contactHeader.setParameter(SIP_INSTANCE_PARAM_NAME, sipInstance);
238 
239         // Add CPM feature tag.
240         uri.setTransportParam(configuration.getSipTransport());
241         contactHeader.setParameter(ICSI_REF_PARAM_NAME, CPM_SESSION_FEATURE_TAG_PARAM_VALUE);
242 
243         return contactHeader;
244     }
245 
246     /**
247      * Create a SIP BYE request for terminating the chat session.
248      *
249      * @param invite the initial INVITE request of the chat session.
250      */
buildBye(SIPRequest invite)251     public static SIPRequest buildBye(SIPRequest invite) throws ParseException {
252         SIPRequest request = new SIPRequest();
253         request.setRequestURI(invite.getRequestURI());
254         request.setMethod(Request.BYE);
255         try {
256             long cSeqNumber = invite.getCSeq().getSeqNumber();
257             request.setHeader(sHeaderFactory.createCSeqHeader(cSeqNumber, Request.BYE));
258         } catch (InvalidArgumentException e) {
259             // Nothing to do here
260         }
261 
262         request.setCallId(invite.getCallId());
263 
264         Via via = (Via) invite.getTopmostVia().clone();
265         via.removeParameter("branch");
266         via.setBranch(Utils.getInstance().generateBranchId());
267         request.addHeader(via);
268         request.addHeader(
269                 sHeaderFactory.createFromHeader(invite.getFrom().getAddress(),
270                         invite.getFrom().getTag()));
271         request.addHeader(
272                 sHeaderFactory.createToHeader(invite.getTo().getAddress(),
273                         invite.getTo().getTag()));
274 
275         return request;
276     }
277 
278     /**
279      * Create SIP INVITE response for a CPM 1:1 chat.
280      *
281      * @param configuration The SipSessionConfiguration instance used for populating SIP headers.
282      * @param invite        the initial INVITE request of the chat session.
283      * @param code          The status code of the response.
284      */
buildInviteResponse( SipSessionConfiguration configuration, SIPRequest invite, int code, @Nullable SimpleSdpMessage sdp)285     public static SIPResponse buildInviteResponse(
286             SipSessionConfiguration configuration,
287             SIPRequest invite,
288             int code,
289             @Nullable SimpleSdpMessage sdp)
290             throws ParseException {
291         SIPResponse response = invite.createResponse(code);
292         if (code == Response.OK) {
293             response.setMessageContent(SDP_CONTENT_TYPE, SDP_CONTENT_SUB_TYPE, sdp.encode());
294         }
295         response.setToTag(Utils.getInstance().generateTag());
296 
297         // Set a Contact header.
298         response.setHeader(generateContactHeader(configuration));
299 
300         // Set Conversation-ID and Contribution-ID
301         Header conversationIdHeader = invite.getHeader(CONVERSATION_ID_HEADER_NAME);
302         if (conversationIdHeader != null) {
303             response.setHeader((Header) conversationIdHeader.clone());
304         }
305         Header contributionIdHeader = invite.getHeader(CONTRIBUTION_ID_HEADER_NAME);
306         if (conversationIdHeader != null) {
307             response.setHeader((Header) contributionIdHeader.clone());
308         }
309 
310         // Set P-Preferred-Identity
311         List<String> associatedUris = configuration.getAssociatedUris();
312         String preferredUri = Iterables.getFirst(associatedUris,
313                 configuration.getPublicUserIdentity());
314         response.setHeader(
315                 sHeaderFactory.createHeader(PPreferredIdentityHeader.NAME, preferredUri));
316 
317         // Set PANI and PLANI if exists
318         if (configuration.getPaniHeader() != null) {
319             response.setHeader(
320                     sHeaderFactory.createHeader(PANI_HEADER_NAME, configuration.getPaniHeader()));
321         }
322         if (configuration.getPlaniHeader() != null) {
323             response.setHeader(
324                     sHeaderFactory.createHeader(PLANI_HEADER_NAME, configuration.getPlaniHeader()));
325         }
326         return response;
327     }
328 
isIPv6Address(String address)329     public static boolean isIPv6Address(String address) {
330         return InetAddresses.forString(address) instanceof Inet6Address;
331     }
332 
333     /** Return whether the SIP message has a SDP content or not */
hasSdpContent(SIPMessage message)334     public static boolean hasSdpContent(SIPMessage message) {
335         ContentType contentType = message.getContentTypeHeader();
336         return contentType != null
337                 && TextUtils.equals(contentType.getContentType(), SDP_CONTENT_TYPE)
338                 && TextUtils.equals(contentType.getContentSubType(), SDP_CONTENT_SUB_TYPE);
339     }
340 
determineTransportBySize(SipSessionConfiguration configuration, int size)341     private static String determineTransportBySize(SipSessionConfiguration configuration,
342             int size) {
343         if (size > configuration.getMaxPayloadSizeOnUdp()) {
344             return "tcp";
345         }
346         return "udp";
347     }
348 }
349