/* * TeleStax, Open Source Cloud Communications * Copyright 2011-2015, Telestax Inc and individual contributors * by the @authors tag. * * This program is free software: you can redistribute it and/or modify * under the terms of the GNU Affero General Public License as * published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> * * For questions related to commercial use licensing, please contact sales@telestax.com. * */ package org.restcomm.android.sdk.SignalingClient.JainSipClient; import android.gov.nist.javax.sip.header.ExtensionHeaderImpl; import android.javax.sip.InvalidArgumentException; import android.javax.sip.ListeningPoint; import android.javax.sip.PeerUnavailableException; import android.javax.sip.ServerTransaction; import android.javax.sip.SipException; import android.javax.sip.SipFactory; import android.javax.sip.SipProvider; import android.javax.sip.address.Address; import android.javax.sip.address.AddressFactory; import android.javax.sip.address.SipURI; import android.javax.sip.address.URI; import android.javax.sip.header.ContactHeader; import android.javax.sip.header.ContentTypeHeader; import android.javax.sip.header.ExpiresHeader; import android.javax.sip.header.Header; import android.javax.sip.header.HeaderFactory; import android.javax.sip.header.ReasonHeader; import android.javax.sip.header.RouteHeader; import android.javax.sip.header.SupportedHeader; import android.javax.sip.header.UserAgentHeader; import android.javax.sip.header.ViaHeader; import android.javax.sip.message.Message; import android.javax.sip.message.MessageFactory; import android.javax.sip.message.Request; import android.javax.sip.message.Response; import org.restcomm.android.sdk.BuildConfig; import org.restcomm.android.sdk.RCClient; import org.restcomm.android.sdk.RCConnection; import org.restcomm.android.sdk.RCDevice; import org.restcomm.android.sdk.util.RCLogger; import java.text.ParseException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; class JainSipMessageBuilder { private HeaderFactory jainSipHeaderFactory; private AddressFactory jainSipAddressFactory; private MessageFactory jainSipMessageFactory; private SipProvider jainSipProvider; private static final String TAG = "JainSipMessageBuilder"; private static final int MAX_FORWARDS = 70; private static final String USERAGENT_STRING = "TelScale Restcomm Android Client " + BuildConfig.VERSION_NAME + "#" + BuildConfig.VERSION_CODE; //"TelScale Restcomm Android Client 1.0.0-BETA4#20"; void initialize(SipFactory sipFactory, SipProvider provider) throws PeerUnavailableException { jainSipHeaderFactory = sipFactory.createHeaderFactory(); jainSipAddressFactory = sipFactory.createAddressFactory(); jainSipMessageFactory = sipFactory.createMessageFactory(); jainSipProvider = provider; } void shutdown() { jainSipHeaderFactory = null; jainSipAddressFactory = null; jainSipMessageFactory = null; } public HeaderFactory getHeaderFactory() { return jainSipHeaderFactory; } // Base request builder common for all requests private Request buildBaseRequest(String method, String username, String domain, String toSipUri, ListeningPoint listeningPoint, HashMap<String, Object> clientContext) throws JainSipException { try { String fromSipUri; // Add route header with the proxy first, if proxy exists (i.e. ) if (domain != null && !domain.equals("")) { // non registrar-less; use username@domain logic fromSipUri = "sip:" + username + "@" + sipUri2IpAddress(domain); } else { // registrar-less fromSipUri = "sip:" + username + "@" + listeningPoint.getIPAddress(); } Address fromAddress = jainSipAddressFactory.createAddress(fromSipUri); fromAddress.setDisplayName(username); Address toAddress; URI requestUri; if (method.equals(Request.REGISTER)) { // register toAddress = fromAddress; requestUri = jainSipAddressFactory.createAddress(domain).getURI(); } else { // non register // remove spaces and dashes from the sip uri String cleanUri = toSipUri.replace(" ", "").replace("-", ""); toAddress = jainSipAddressFactory.createAddress(cleanUri); requestUri = jainSipAddressFactory.createURI(cleanUri); } Request request = jainSipMessageFactory.createRequest(requestUri, method, jainSipProvider.getNewCallId(), jainSipHeaderFactory.createCSeqHeader(1l, method), jainSipHeaderFactory.createFromHeader(fromAddress, Long.toString(System.currentTimeMillis())), jainSipHeaderFactory.createToHeader(toAddress, null), createViaHeaders(listeningPoint), jainSipHeaderFactory.createMaxForwardsHeader(MAX_FORWARDS)); // Add route header with the proxy first, if proxy exists (i.e. non registrar-less) if (domain != null && !domain.equals("")) { RouteHeader routeHeader = createRouteHeader(domain); request.addFirst(routeHeader); } // Only pass registering domain non null in register requests String registeringDomain = null; if (method.equals(Request.REGISTER)) { registeringDomain = domain; } Address contactAddress = createContactAddress(listeningPoint, registeringDomain, clientContext); ContactHeader contactHeader = jainSipHeaderFactory.createContactHeader(contactAddress); request.addHeader(contactHeader); request.addHeader(createUserAgentHeader()); return request; } catch (ParseException e) { if (method.equals(Request.REGISTER)) { throw new JainSipException(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_URI_INVALID, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_URI_INVALID), e); } else if (method.equals(Request.INVITE)) { throw new JainSipException(RCClient.ErrorCodes.ERROR_CONNECTION_URI_INVALID, RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_URI_INVALID), e); } else { throw new JainSipException(RCClient.ErrorCodes.ERROR_MESSAGE_URI_INVALID, RCClient.errorText(RCClient.ErrorCodes.ERROR_MESSAGE_URI_INVALID), e); } } catch (JainSipException e) { throw e; } catch (Exception e) { // TODO: let's emit a RuntimeException for now so that we get a loud and clear indication of issues involved in the field and then // we can adjust and only do a e.printStackTrace() throw new RuntimeException("Failed to build base SIP request", e); } } public Request buildRegisterRequest(ListeningPoint listeningPoint, int expires, HashMap<String, Object> parameters) throws JainSipException { try { Request request = buildBaseRequest(Request.REGISTER, (String) parameters.get(RCDevice.ParameterKeys.SIGNALING_USERNAME), (String) parameters.get(RCDevice.ParameterKeys.SIGNALING_DOMAIN), null, listeningPoint, null); ExpiresHeader expiresHeader = jainSipHeaderFactory.createExpiresHeader(expires); request.addHeader(expiresHeader); return request; } catch (JainSipException e) { throw e; } catch (Exception e) { // TODO: let's emit a RuntimeException for now so that we get a loud and clear indication of issues involved in the field and then // we can adjust and only do a e.printStackTrace() throw new RuntimeException("Failed to build SIP Register request", e); } } public Request buildInviteRequest(ListeningPoint listeningPoint, HashMap<String, Object> parameters, HashMap<String, Object> clientConfiguration, HashMap<String, Object> clientContext) throws JainSipException { try { Request request = buildBaseRequest(Request.INVITE, (String) clientConfiguration.get(RCDevice.ParameterKeys.SIGNALING_USERNAME), (String) clientConfiguration.get(RCDevice.ParameterKeys.SIGNALING_DOMAIN), (String) parameters.get(RCConnection.ParameterKeys.CONNECTION_PEER), listeningPoint, clientContext); SupportedHeader supportedHeader = jainSipHeaderFactory.createSupportedHeader("replaces, outbound"); request.addHeader(supportedHeader); // Create ContentTypeHeader ContentTypeHeader contentTypeHeader = jainSipHeaderFactory.createContentTypeHeader("application", "sdp"); byte[] contents = ((String) parameters.get("sdp")).getBytes(); request.setContent(contents, contentTypeHeader); // add custom sip headers if applicable if (parameters.containsKey(RCConnection.ParameterKeys.CONNECTION_CUSTOM_SIP_HEADERS)) { try { addCustomHeaders(request, (HashMap<String, String>) parameters.get(RCConnection.ParameterKeys.CONNECTION_CUSTOM_SIP_HEADERS)); } catch (ParseException e) { throw new JainSipException(RCClient.ErrorCodes.ERROR_CONNECTION_PARSE_CUSTOM_SIP_HEADERS, RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_PARSE_CUSTOM_SIP_HEADERS), e); } } return request; } catch (JainSipException e) { throw e; } catch (ParseException e) { throw new JainSipException(RCClient.ErrorCodes.ERROR_CONNECTION_URI_INVALID, RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_URI_INVALID), e); } catch (Exception e) { // TODO: let's emit a RuntimeException for now so that we get a loud and clear indication of issues involved in the field and then // we can adjust and only do a e.printStackTrace() throw new RuntimeException("Failed to build SIP Invite request", e); } } public Request buildMessageRequest(String toSipUri, String message, ListeningPoint listeningPoint, HashMap<String, Object> clientConfiguration) throws JainSipException { try { Request request = buildBaseRequest(Request.MESSAGE, (String) clientConfiguration.get(RCDevice.ParameterKeys.SIGNALING_USERNAME), (String) clientConfiguration.get(RCDevice.ParameterKeys.SIGNALING_DOMAIN), toSipUri, listeningPoint, null); SupportedHeader supportedHeader = jainSipHeaderFactory.createSupportedHeader("replaces, outbound"); request.addHeader(supportedHeader); ContentTypeHeader contentTypeHeader = jainSipHeaderFactory.createContentTypeHeader("text", "plain"); request.setContent(message, contentTypeHeader); return request; } catch (JainSipException e) { throw e; } catch (ParseException e) { throw new JainSipException(RCClient.ErrorCodes.ERROR_MESSAGE_URI_INVALID, RCClient.errorText(RCClient.ErrorCodes.ERROR_MESSAGE_URI_INVALID), e); } catch (Exception e) { // TODO: let's emit a RuntimeException for now so that we get a loud and clear indication of issues involved in the field and then // we can adjust and only do a e.printStackTrace() throw new RuntimeException("Failed to build SIP Message request", e); } } public Request buildByeRequest(android.javax.sip.Dialog dialog, String reason, HashMap<String, Object> clientConfiguration) throws JainSipException { try { Request request = dialog.createRequest(Request.BYE); request.addHeader(createUserAgentHeader()); if (clientConfiguration.containsKey(RCDevice.ParameterKeys.SIGNALING_DOMAIN) && !clientConfiguration.get(RCDevice.ParameterKeys.SIGNALING_DOMAIN).equals("")) { // we only need this for non-registrarless calls since the problem is only for incoming calls, // and when working in registrarless mode there are no incoming calls RouteHeader routeHeader = createRouteHeader((String) clientConfiguration.get(RCDevice.ParameterKeys.SIGNALING_DOMAIN)); request.addFirst(routeHeader); } int reasonCode; if (reason == null || reason.isEmpty()) { // normal hangup reason = "Normal-Hangup"; reasonCode = 0; } else { // error, the only error that we convey right now is the connectivity drop reasonCode = 1; } ReasonHeader reasonHeader = jainSipHeaderFactory.createReasonHeader("SIP", reasonCode, reason); request.addHeader(reasonHeader); return request; } catch (ParseException|InvalidArgumentException e) { // these exceptions occur only in reason header generation, so if it happens it means there's programming error we need to fix throw new RuntimeException("Error generating Reason Header for request", e); } catch (SipException e) { throw new JainSipException(RCClient.ErrorCodes.ERROR_CONNECTION_DISCONNECT_FAILED, RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_DISCONNECT_FAILED), e); } } public Request buildDtmfInfoRequest(android.javax.sip.Dialog dialog, String digits) throws JainSipException { try { Request request = dialog.createRequest(Request.INFO); /* // increase Cseq CSeqHeader cseq = (CSeqHeader) jainSipJob.transaction.getRequest().getHeader(CSeqHeader.NAME); long seqNumber = cseq.getSeqNumber(); cseq.setSeqNumber(++seqNumber); request.setHeader(cseq); */ request.setContent("Signal=" + digits + "\r\nDuration=100\r\n", jainSipHeaderFactory.createContentTypeHeader("application", "dtmf-relay")); return request; } catch (Exception e) { throw new JainSipException(RCClient.ErrorCodes.ERROR_CONNECTION_DTMF_DIGITS_FAILED, RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_DTMF_DIGITS_FAILED), e); } } public Response buildInvite200OKResponse(ServerTransaction transaction, String sdp, ListeningPoint listeningPoint, HashMap<String, Object> clientContext) throws JainSipException { try { Response response = jainSipMessageFactory.createResponse(Response.OK, transaction.getRequest()); Address contactAddress = createContactAddress(listeningPoint, null, clientContext); ContactHeader contactHeader = jainSipHeaderFactory.createContactHeader(contactAddress); response.addHeader(contactHeader); // Not needed as it is being set when sending 180 Ringing //ToHeader toHeader = (ToHeader) response.getHeader(ToHeader.NAME); //toHeader.setTag(Long.toString(System.currentTimeMillis())); //response.addHeader(contactHeader); ContentTypeHeader contentTypeHeader = jainSipHeaderFactory.createContentTypeHeader("application", "sdp"); response.setContent(sdp.getBytes(), contentTypeHeader); return response; } catch (ParseException e) { throw new JainSipException(RCClient.ErrorCodes.ERROR_CONNECTION_ACCEPT_FAILED, RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_ACCEPT_FAILED), e); } } public Response buildResponse(int responseType, Request request) { Response response; try { response = jainSipMessageFactory.createResponse(responseType, request); //RCLogger.v(TAG, "Sending SIP response: \n" + response.toString()); return response; } catch (ParseException e) { throw new RuntimeException("Error creating response", e); } } public Response buildOptions200OKResponse(Request request, ListeningPoint listeningPoint) throws JainSipException { Response response; try { response = jainSipMessageFactory.createResponse(Response.OK, request); Address contactAddress = createContactAddress(listeningPoint, null, null); ContactHeader contactHeader = jainSipHeaderFactory.createContactHeader(contactAddress); response.addHeader(contactHeader); response.removeHeader("P-Asserted-Identity"); response.removeHeader("P-Charging-Vector"); response.removeHeader("P-Charging-Function-Addresses"); response.removeHeader("P-Called-Party-ID"); return response; } catch (ParseException e) { throw new RuntimeException("Error creating Options 200 OK response", e); } } /* public Response build200OK(Request request) { Response response; try { response = jainSipMessageFactory.createResponse(200, request); RCLogger.v(TAG, "Sending SIP response: \n" + response.toString()); return response; } catch (ParseException e) { throw new RuntimeException("Error creating 200 OK"); } } */ // -- Helpers // Take a short destination of the form 'bob' and create full SIP URI out of it: 'sip:bob@cloud.restcomm.com' public String convert2FullUri(String usernameOrUri, String domain) throws JainSipException { String fullUri = usernameOrUri; if (!usernameOrUri.contains("sip:")) { if (domain == null || domain.equals("")) { throw new JainSipException(RCClient.ErrorCodes.ERROR_CONNECTION_REGISTRARLESS_FULL_URI_REQUIRED, RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_REGISTRARLESS_FULL_URI_REQUIRED)); } fullUri = "sip:" + usernameOrUri + "@" + domain.replaceAll("sip:", ""); RCLogger.i(TAG, "convert2FullUri(): normalizing username to: " + fullUri); } else { RCLogger.i(TAG, "convert2FullUri(): no need for normalization, URI already normalized: " + fullUri); } return fullUri; } // Take a short domain of the form 'cloud.restcomm.com' and create full SIP domain out of it: 'sip:cloud.restcomm.com' public String convertDomain2Uri(String domain) { String domainUri = domain; // when domain is empty (i.e. registrar-less we don't want to touch it) if (!domain.isEmpty() && !domain.contains("sip:")) { domainUri = "sip:" + domain; RCLogger.i(TAG, "convertDomain2Uri(): normalizing domain to: " + domainUri); } else { RCLogger.i(TAG, "convertDomain2Uri(): no need for normalization, URI already normalized: " + domainUri); } return domainUri; } // Normalize domain and SIP URIs public void normalizeDomain(HashMap<String, Object> parameters) { if (parameters.containsKey(RCDevice.ParameterKeys.SIGNALING_DOMAIN)) { parameters.put(RCDevice.ParameterKeys.SIGNALING_DOMAIN, convertDomain2Uri((String) parameters.get(RCDevice.ParameterKeys.SIGNALING_DOMAIN))); } } public void normalizePeer(HashMap<String, Object> peerParameters, HashMap<String, Object> clientParameters) throws JainSipException { if (peerParameters.containsKey(RCConnection.ParameterKeys.CONNECTION_PEER)) { peerParameters.put(RCConnection.ParameterKeys.CONNECTION_PEER, convert2FullUri((String) peerParameters.get(RCConnection.ParameterKeys.CONNECTION_PEER), (String) clientParameters.get(RCDevice.ParameterKeys.SIGNALING_DOMAIN))); } } private RouteHeader createRouteHeader(String route) { try { SipURI routeUri = (SipURI) jainSipAddressFactory.createURI(route); routeUri.setLrParam(); Address routeAddress = jainSipAddressFactory.createAddress(routeUri); return jainSipHeaderFactory.createRouteHeader(routeAddress); } catch (ParseException e) { throw new RuntimeException("Error creating SIP Route header", e); } } private void addCustomHeaders(Request callRequest, HashMap<String, String> sipHeaders) throws ParseException { if (sipHeaders != null) { // Get a set of the entries Set set = sipHeaders.entrySet(); // Get an iterator Iterator i = set.iterator(); // Display elements while (i.hasNext()) { Map.Entry me = (Map.Entry) i.next(); Header customHeader = jainSipHeaderFactory.createHeader(me.getKey().toString(), me.getValue().toString()); callRequest.addHeader(customHeader); } } } // convert sip uri, like sip:cloud.restcomm.com:5060 -> cloud.restcomm.com public String sipUri2IpAddress(String sipUri) throws ParseException, JainSipException { try { Address address = jainSipAddressFactory.createAddress(sipUri); return ((SipURI) address.getURI()).getHost(); } catch (ClassCastException e) { throw new JainSipException(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_URI_INVALID, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_URI_INVALID), e); } } public ArrayList<ViaHeader> createViaHeaders(ListeningPoint listeningPoint) throws ParseException { ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>(); try { ViaHeader viaHeader = jainSipHeaderFactory.createViaHeader(listeningPoint.getIPAddress(), listeningPoint.getPort(), listeningPoint.getTransport(), null); viaHeader.setRPort(); viaHeaders.add(viaHeader); } catch (InvalidArgumentException e) { throw new RuntimeException("Failed to create Via headers", e); } return viaHeaders; } public Address createContactAddress(ListeningPoint listeningPoint, String domain, HashMap<String, Object> clientContext) throws ParseException, JainSipException { RCLogger.i(TAG, "createContactAddress()"); int contactPort = listeningPoint.getPort(); String contactIPAddress = listeningPoint.getIPAddress(); // Create the contact address. If we have Via received/rport populated we need to use those, otherwise just use the local listening point's if (clientContext != null) { if (clientContext.containsKey("via-rport")) { contactPort = (int) clientContext.get("via-rport"); } if (clientContext.containsKey("via-received")) { contactIPAddress = (String) clientContext.get("via-received"); } } String contactString = getContactString(contactIPAddress, contactPort, listeningPoint.getTransport(), domain); return jainSipAddressFactory.createAddress(contactString); } public String getContactString(ListeningPoint listeningPoint, String domain) throws ParseException, JainSipException { return getContactString(listeningPoint.getIPAddress(), listeningPoint.getPort(), listeningPoint.getTransport(), domain); } public String getContactString(String ipAddress, int port, String transport, String domain) throws ParseException, JainSipException { String contactString = "sip:" + ipAddress + ':' + port + ";transport=" + transport; if (domain != null && !domain.equals("")) { contactString += ";registering_acc=" + sipUri2IpAddress(domain); } return contactString; } public UserAgentHeader createUserAgentHeader() { RCLogger.i(TAG, "createUserAgentHeader()"); List<String> userAgentTokens = new LinkedList<String>(); UserAgentHeader header = null; userAgentTokens.add(USERAGENT_STRING); try { header = jainSipHeaderFactory.createUserAgentHeader(userAgentTokens); } catch (ParseException e) { throw new RuntimeException("Error creating User Agent header", e); } return header; } /* * Parse a SIP request/response (i.e. Message) and extract any custom sip headers inside a HashMap where key is the header name and value is the header value */ static public HashMap<String, String> parseCustomHeaders(Message message) { // See if there are any custom SIP headers and expose them. Custom headers are headers starting with 'X-' HashMap<String,String> customHeaders = new HashMap<>(); ListIterator iterator = message.getHeaderNames(); while (iterator.hasNext()) { String headerName = (String)iterator.next(); if (headerName.matches("(?s)^X-.*")) { ExtensionHeaderImpl header = (ExtensionHeaderImpl)message.getHeader(headerName); customHeaders.put(header.getName(), header.getHeaderValue()); } } if (customHeaders.isEmpty()) { customHeaders = null; } return customHeaders; } }