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