blob: 13fa53a7f438b6e083df2b5651d1e3db4c57525e [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.libraries.rcs.simpleclient.protocol.sip;
import static com.android.libraries.rcs.simpleclient.protocol.sdp.SdpUtils.SDP_CONTENT_SUB_TYPE;
import static com.android.libraries.rcs.simpleclient.protocol.sdp.SdpUtils.SDP_CONTENT_TYPE;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import com.android.libraries.rcs.simpleclient.protocol.sdp.SimpleSdpMessage;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.net.InetAddresses;
import gov.nist.javax.sip.Utils;
import gov.nist.javax.sip.address.AddressFactoryImpl;
import gov.nist.javax.sip.header.ContentType;
import gov.nist.javax.sip.header.HeaderFactoryImpl;
import gov.nist.javax.sip.header.Via;
import gov.nist.javax.sip.header.extensions.SessionExpires;
import gov.nist.javax.sip.header.ims.PPreferredIdentityHeader;
import gov.nist.javax.sip.header.ims.PPreferredServiceHeader;
import gov.nist.javax.sip.header.ims.SecurityVerifyHeader;
import gov.nist.javax.sip.message.SIPMessage;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import java.net.Inet6Address;
import java.text.ParseException;
import java.util.List;
import java.util.UUID;
import javax.sip.InvalidArgumentException;
import javax.sip.address.AddressFactory;
import javax.sip.address.SipURI;
import javax.sip.address.URI;
import javax.sip.header.ContactHeader;
import javax.sip.header.Header;
import javax.sip.header.HeaderFactory;
import javax.sip.header.ViaHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
/** Collections of utility functions for SIP */
public final class SipUtils {
private static final String TAG = "SipUtils";
private static final String SUPPORTED_TIMER_TAG = "timer";
private static final String ICSI_REF_PARAM_NAME = "+g.3gpp.icsi-ref";
private static final String SIP_INSTANCE_PARAM_NAME = "+sip.instance";
private static final String CPM_SESSION_FEATURE_TAG_PARAM_VALUE =
"\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"";
private static final String CPM_SESSION_FEATURE_TAG_FULL_STRING =
"+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"";
private static final String CPM_SESSION_SERVICE_NAME =
"urn:urn-7:3gpp-service.ims.icsi.oma.cpm.session";
private static final String CONTRIBUTION_ID_HEADER_NAME = "Contribution-ID";
private static final String CONVERSATION_ID_HEADER_NAME = "Conversation-ID";
private static final String ACCEPT_CONTACT_HEADER_NAME = "Accept-Contact";
private static final String PANI_HEADER_NAME = "P-Access-Network-Info";
private static final String PLANI_HEADER_NAME = "P-Last-Access-Network-Info";
private static final String USER_AGENT_HEADER = "RcsTestClient";
private static AddressFactory sAddressFactory = new AddressFactoryImpl();
private static HeaderFactory sHeaderFactory = new HeaderFactoryImpl();
private SipUtils() {
}
/**
* Try to parse the given uri.
*
* @throws IllegalArgumentException in case of parsing error.
*/
public static URI createUri(String uri) {
try {
return sAddressFactory.createURI(uri);
} catch (ParseException exception) {
throw new IllegalArgumentException("URI cannot be created", exception);
}
}
/**
* Create SIP INVITE request for a CPM 1:1 chat.
*
* @param configuration The SipSessionConfiguration instance used for populating SIP headers.
* @param targetUri The uri to be targeted.
* @param conversationId The id to be contained in Conversation-ID header.
*/
public static SIPRequest buildInvite(
SipSessionConfiguration configuration,
String targetUri,
String conversationId,
byte[] content)
throws ParseException {
String address = configuration.getLocalIpAddress();
int port = configuration.getLocalPort();
String transport = configuration.getSipTransport();
List<String> associatedUris = configuration.getAssociatedUris();
String preferredUri = Iterables.getFirst(associatedUris,
configuration.getPublicUserIdentity());
SIPRequest request = new SIPRequest();
request.setMethod(Request.INVITE);
URI remoteUri = createUri(targetUri);
request.setRequestURI(remoteUri);
request.setFrom(
sHeaderFactory.createFromHeader(
sAddressFactory.createAddress(preferredUri),
Utils.getInstance().generateTag()));
request.setTo(
sHeaderFactory.createToHeader(sAddressFactory.createAddress(remoteUri), null));
ViaHeader viaHeader = null;
try {
// Set a default Max-Forwards header.
request.setMaxForwards(sHeaderFactory.createMaxForwardsHeader(70));
request.setCSeq(sHeaderFactory.createCSeqHeader(1L, Request.INVITE));
viaHeader =
sHeaderFactory.createViaHeader(
address, port, transport, Utils.getInstance().generateBranchId());
request.setVia(ImmutableList.of(viaHeader));
// Set a default Session-Expires header.
SessionExpires sessionExpires = new SessionExpires();
sessionExpires.setRefresher("uac");
sessionExpires.setExpires(1800);
request.setHeader(sessionExpires);
// Set a Contact header.
request.setHeader(generateContactHeader(configuration));
// Set PANI and PLANI if exists
if (configuration.getPaniHeader() != null) {
request.setHeader(
sHeaderFactory.createHeader(PANI_HEADER_NAME,
configuration.getPaniHeader()));
}
if (configuration.getPlaniHeader() != null) {
request.setHeader(
sHeaderFactory.createHeader(PLANI_HEADER_NAME,
configuration.getPlaniHeader()));
}
} catch (InvalidArgumentException e) {
// Nothing to do here
Log.e(TAG, e.getMessage());
}
request.setCallId(UUID.randomUUID().toString());
request.setHeader(sHeaderFactory.createHeader(CONVERSATION_ID_HEADER_NAME, conversationId));
request.setHeader(
sHeaderFactory.createHeader(CONTRIBUTION_ID_HEADER_NAME,
UUID.randomUUID().toString()));
String acceptContact = "*;" + CPM_SESSION_FEATURE_TAG_FULL_STRING;
request.setHeader(sHeaderFactory.createHeader(ACCEPT_CONTACT_HEADER_NAME, acceptContact));
request.setHeader(sHeaderFactory.createSupportedHeader(SUPPORTED_TIMER_TAG));
request.setHeader(sHeaderFactory.createHeader(PPreferredIdentityHeader.NAME, preferredUri));
request.setHeader(
sHeaderFactory.createHeader(PPreferredServiceHeader.NAME,
CPM_SESSION_SERVICE_NAME));
// Set a Security-Verify header if exist.
String securityVerify = configuration.getSecurityVerifyHeader();
if (!TextUtils.isEmpty(securityVerify)) {
request.setHeader(
sHeaderFactory.createHeader(SecurityVerifyHeader.NAME, securityVerify));
}
// Add Route headers.
List<String> serviceRoutes = configuration.getServiceRouteHeaders();
if (!serviceRoutes.isEmpty()) {
for (String sr : serviceRoutes) {
request.addHeader(
sHeaderFactory.createRouteHeader(sAddressFactory.createAddress(sr)));
}
}
String userAgent = configuration.getUserAgentHeader();
userAgent = (userAgent == null) ? USER_AGENT_HEADER : userAgent;
request.addHeader(sHeaderFactory.createUserAgentHeader(ImmutableList.of(userAgent)));
request.setMessageContent(SDP_CONTENT_TYPE, SDP_CONTENT_SUB_TYPE, content);
if (viaHeader != null && Ascii.equalsIgnoreCase("udp", transport)) {
String newTransport =
determineTransportBySize(configuration, request.encodeAsBytes("udp").length);
if (!Ascii.equalsIgnoreCase(transport, newTransport)) {
viaHeader.setTransport(newTransport);
}
}
return request;
}
private static ContactHeader generateContactHeader(SipSessionConfiguration configuration)
throws ParseException {
String host = configuration.getLocalIpAddress();
if (isIPv6Address(host)) {
host = "[" + host + "]";
}
String userPart = configuration.getContactUser();
SipURI uri = sAddressFactory.createSipURI(userPart, host);
try {
uri.setPort(configuration.getLocalPort());
uri.setTransportParam(configuration.getSipTransport());
} catch (Exception e) {
// Shouldn't be here.
}
ContactHeader contactHeader =
sHeaderFactory.createContactHeader(sAddressFactory.createAddress(uri));
// Add +sip.instance param.
String sipInstance = "\"<urn:gsma:imei:" + configuration.getImei() + ">\"";
contactHeader.setParameter(SIP_INSTANCE_PARAM_NAME, sipInstance);
// Add CPM feature tag.
uri.setTransportParam(configuration.getSipTransport());
contactHeader.setParameter(ICSI_REF_PARAM_NAME, CPM_SESSION_FEATURE_TAG_PARAM_VALUE);
return contactHeader;
}
/**
* Create a SIP BYE request for terminating the chat session.
*
* @param invite the initial INVITE request of the chat session.
*/
public static SIPRequest buildBye(SIPRequest invite) throws ParseException {
SIPRequest request = new SIPRequest();
request.setRequestURI(invite.getRequestURI());
request.setMethod(Request.BYE);
try {
long cSeqNumber = invite.getCSeq().getSeqNumber();
request.setHeader(sHeaderFactory.createCSeqHeader(cSeqNumber, Request.BYE));
} catch (InvalidArgumentException e) {
// Nothing to do here
}
request.setCallId(invite.getCallId());
Via via = (Via) invite.getTopmostVia().clone();
via.removeParameter("branch");
via.setBranch(Utils.getInstance().generateBranchId());
request.addHeader(via);
request.addHeader(
sHeaderFactory.createFromHeader(invite.getFrom().getAddress(),
invite.getFrom().getTag()));
request.addHeader(
sHeaderFactory.createToHeader(invite.getTo().getAddress(),
invite.getTo().getTag()));
return request;
}
/**
* Create SIP INVITE response for a CPM 1:1 chat.
*
* @param configuration The SipSessionConfiguration instance used for populating SIP headers.
* @param invite the initial INVITE request of the chat session.
* @param code The status code of the response.
*/
public static SIPResponse buildInviteResponse(
SipSessionConfiguration configuration,
SIPRequest invite,
int code,
@Nullable SimpleSdpMessage sdp)
throws ParseException {
SIPResponse response = invite.createResponse(code);
if (code == Response.OK) {
response.setMessageContent(SDP_CONTENT_TYPE, SDP_CONTENT_SUB_TYPE, sdp.encode());
}
response.setToTag(Utils.getInstance().generateTag());
// Set a Contact header.
response.setHeader(generateContactHeader(configuration));
// Set Conversation-ID and Contribution-ID
Header conversationIdHeader = invite.getHeader(CONVERSATION_ID_HEADER_NAME);
if (conversationIdHeader != null) {
response.setHeader((Header) conversationIdHeader.clone());
}
Header contributionIdHeader = invite.getHeader(CONTRIBUTION_ID_HEADER_NAME);
if (conversationIdHeader != null) {
response.setHeader((Header) contributionIdHeader.clone());
}
// Set P-Preferred-Identity
List<String> associatedUris = configuration.getAssociatedUris();
String preferredUri = Iterables.getFirst(associatedUris,
configuration.getPublicUserIdentity());
response.setHeader(
sHeaderFactory.createHeader(PPreferredIdentityHeader.NAME, preferredUri));
// Set PANI and PLANI if exists
if (configuration.getPaniHeader() != null) {
response.setHeader(
sHeaderFactory.createHeader(PANI_HEADER_NAME, configuration.getPaniHeader()));
}
if (configuration.getPlaniHeader() != null) {
response.setHeader(
sHeaderFactory.createHeader(PLANI_HEADER_NAME, configuration.getPlaniHeader()));
}
return response;
}
public static boolean isIPv6Address(String address) {
return InetAddresses.forString(address) instanceof Inet6Address;
}
/** Return whether the SIP message has a SDP content or not */
public static boolean hasSdpContent(SIPMessage message) {
ContentType contentType = message.getContentTypeHeader();
return contentType != null
&& TextUtils.equals(contentType.getContentType(), SDP_CONTENT_TYPE)
&& TextUtils.equals(contentType.getContentSubType(), SDP_CONTENT_SUB_TYPE);
}
private static String determineTransportBySize(SipSessionConfiguration configuration,
int size) {
if (size > configuration.getMaxPayloadSizeOnUdp()) {
return "tcp";
}
return "udp";
}
}